class AccountCheck(models.Model): _name = 'account.check' _description = 'Account Check' _order = "id desc" _inherit = ['mail.thread'] operation_ids = fields.One2many( 'account.check.operation', 'check_id', ) name = fields.Char( required=True, readonly=True, copy=False, states={'draft': [('readonly', False)]}, ) number = fields.Integer(required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False) checkbook_id = fields.Many2one( 'account.checkbook', 'Checkbook', readonly=True, states={'draft': [('readonly', False)]}, ) issue_check_subtype = fields.Selection( related='checkbook_id.issue_check_subtype') type = fields.Selection( [('issue_check', 'Issue Check'), ('third_check', 'Third Check')], readonly=True, ) partner_id = fields.Many2one(related='operation_ids.partner_id', readonly=True, store=True) state = fields.Selection( [ ('draft', 'Draft'), ('holding', 'Holding'), ('deposited', 'Deposited'), ('selled', 'Selled'), ('delivered', 'Delivered'), ('transfered', 'Transfered'), ('reclaimed', 'Reclaimed'), ('withdrawed', 'Withdrawed'), ('handed', 'Handed'), ('rejected', 'Rejected'), ('debited', 'Debited'), ('returned', 'Returned'), ('changed', 'Changed'), ('cancel', 'Cancel'), ], required=True, default='draft', copy=False, compute='_compute_state', store=True, ) issue_date = fields.Date( 'Issue Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, ) owner_vat = fields.Char('Owner Vat', readonly=True, states={'draft': [('readonly', False)]}) owner_name = fields.Char('Owner Name', readonly=True, states={'draft': [('readonly', False)]}) bank_id = fields.Many2one('res.bank', 'Bank', readonly=True, states={'draft': [('readonly', False)]}) amount = fields.Monetary(currency_field='company_currency_id', readonly=True, states={'draft': [('readonly', False)]}) amount_currency = fields.Monetary( currency_field='currency_id', readonly=True, states={'draft': [('readonly', False)]}, ) currency_id = fields.Many2one( 'res.currency', readonly=True, states={'draft': [('readonly', False)]}, ) payment_date = fields.Date(readonly=True, states={'draft': [('readonly', False)]}) journal_id = fields.Many2one('account.journal', string='Journal', required=True, domain=[('type', 'in', ['cash', 'bank'])], readonly=True, states={'draft': [('readonly', False)]}) company_id = fields.Many2one( related='journal_id.company_id', readonly=True, store=True, ) company_currency_id = fields.Many2one( related='company_id.currency_id', readonly=True, ) @api.multi @api.constrains('issue_date', 'payment_date') @api.onchange('issue_date', 'payment_date') def onchange_date(self): for rec in self: if (rec.issue_date and rec.payment_date and rec.issue_date > rec.payment_date): raise UserError( _('Check Payment Date must be greater than Issue Date')) @api.multi @api.constrains( 'type', 'number', ) def issue_number_interval(self): for rec in self: # if not range, then we dont check it if rec.type == 'issue_check' and rec.checkbook_id.range_to: if rec.number > rec.checkbook_id.range_to: raise UserError( _("Check number (%s) can't be greater than %s on " "checkbook %s (%s)") % ( rec.number, rec.checkbook_id.range_to, rec.checkbook_id.name, rec.checkbook_id.id, )) elif rec.number == rec.checkbook_id.range_to: rec.checkbook_id.state = 'used' return False @api.multi @api.constrains( 'type', 'owner_name', 'bank_id', ) def _check_unique(self): for rec in self: if rec.type == 'issue_check': same_checks = self.search([ ('checkbook_id', '=', rec.checkbook_id.id), ('type', '=', rec.type), ('number', '=', rec.number), ]) same_checks -= self if same_checks: raise ValidationError( _('Check Number (%s) must be unique per Checkbook!\n' '* Check ids: %s') % (rec.name, same_checks.ids)) elif self.type == 'third_check': # agregamos condicion de company ya que un cheque de terceros # se puede pasar entre distintas cias same_checks = self.search([ ('company_id', '=', rec.company_id.id), ('bank_id', '=', rec.bank_id.id), ('owner_name', '=', rec.owner_name), ('type', '=', rec.type), ('number', '=', rec.number), ]) same_checks -= self if same_checks: raise ValidationError( _('Check Number (%s) must be unique per Owner and Bank!' '\n* Check ids: %s') % (rec.name, same_checks.ids)) return True @api.multi def _del_operation(self, origin): """ We check that the operation that is being cancel is the last operation done (same as check state) """ for rec in self: if not rec.operation_ids or rec.operation_ids[0].origin != origin: raise ValidationError( _('You can not cancel this operation because this is not ' 'the last operation over the check.\nCheck (id): %s (%s)' ) % (rec.name, rec.id)) rec.operation_ids[0].origin = False rec.operation_ids[0].unlink() @api.multi def _add_operation(self, operation, origin, partner=None, date=False): for rec in self: rec._check_state_change(operation) # agregamos validacion de fechas date = date or fields.Datetime.now() if rec.operation_ids and rec.operation_ids[0].date > date: raise ValidationError( _('The date of a new check operation can not be minor than ' 'last operation date.\n' '* Check Id: %s\n' '* Check Number: %s\n' '* Operation: %s\n' '* Operation Date: %s\n' '* Last Operation Date: %s') % (rec.id, rec.name, operation, date, rec.operation_ids[0].date)) vals = { 'operation': operation, 'date': date, 'check_id': rec.id, 'origin': '%s,%i' % (origin._name, origin.id), 'partner_id': partner and partner.id or False, } rec.operation_ids.create(vals) @api.multi @api.depends( 'operation_ids.operation', 'operation_ids.date', ) def _compute_state(self): for rec in self: if rec.operation_ids: operation = rec.operation_ids[0].operation rec.state = operation else: rec.state = 'draft' @api.multi def _check_state_change(self, operation): """ We only check state change from _add_operation because we want to leave the user the possibility of making anything from interface. Necesitamos este chequeo para evitar, por ejemplo, que un cheque se agregue dos veces en un pago y luego al confirmar se entregue dos veces On operation_from_state_map dictionary: * key is 'to state' * value is 'from states' """ self.ensure_one() # if we do it from _add_operation only, not from a contraint of before # computing the value, we can just read it old_state = self.state operation_from_state_map = { # 'draft': [False], 'holding': ['draft', 'deposited', 'selled', 'delivered', 'transfered'], 'delivered': ['holding'], 'deposited': ['holding', 'rejected'], 'selled': ['holding'], 'handed': ['draft'], 'transfered': ['holding'], 'withdrawed': ['draft'], 'rejected': ['delivered', 'deposited', 'selled', 'handed'], 'debited': ['handed'], 'returned': ['handed', 'holding'], 'changed': ['handed', 'holding'], 'cancel': ['draft'], 'reclaimed': ['rejected'], } from_states = operation_from_state_map.get(operation) if not from_states: raise ValidationError( _('Operation %s not implemented for checks!') % operation) if old_state not in from_states: raise ValidationError( _('You can not "%s" a check from state "%s"!\n' 'Check nbr (id): %s (%s)') % (self.operation_ids._fields['operation'].convert_to_export( operation, self.env), self._fields['state'].convert_to_export( old_state, self.env), self.name, self.id)) @api.multi def unlink(self): for rec in self: if rec.state not in ('draft', 'cancel'): raise ValidationError( _('The Check must be in draft state for unlink !')) return super(AccountCheck, self).unlink() # checks operations from checks @api.multi def bank_debit(self): self.ensure_one() if self.state in ['handed']: vals = self.get_bank_vals('bank_debit', self.journal_id) action_date = self._context.get('action_date') vals['date'] = action_date move = self.env['account.move'].create(vals) self.handed_reconcile(move) move.post() self._add_operation('debited', move, date=action_date) @api.multi def handed_reconcile(self, move): """ Funcion que por ahora solo intenta conciliar cheques propios entregados cuando se hace un debito o cuando el proveedor lo rechaza """ self.ensure_one() debit_account = self.company_id._get_check_account('deferred') # conciliamos if debit_account.reconcile: operation = self._get_operation('handed') if operation.origin._name == 'account.payment': move_lines = operation.origin.move_line_ids elif operation.origin._name == 'account.move': move_lines = operation.origin.line_ids move_lines |= move.line_ids move_lines = move_lines.filtered( lambda x: x.account_id == debit_account) if len(move_lines) != 2: raise ValidationError( _('We have found more or less thant two journal items to ' 'reconcile with check debit.\n' '*Journal items: %s') % move_lines.ids) move_lines.reconcile() @api.model def get_third_check_account(self): """ For third checks, if we use a journal only for third checks, we use accounts on journal, if not we use company account # TODO la idea es depreciar esto y que si se usa cheques de terceros se use la misma cuenta que la del diario y no la cuenta configurada en la cia, lo dejamos por ahora por nosotros y 4 clientes que estan asi """ # self.ensure_one() # desde los pagos, pueden venir mas de un cheque pero para que # funcione bien, todos los cheques deberian usar la misma cuenta, # hacemos esa verificación account = self.env['account.account'] for rec in self: credit_account = rec.journal_id.default_credit_account_id debit_account = rec.journal_id.default_debit_account_id inbound_methods = rec.journal_id['inbound_payment_method_ids'] outbound_methods = rec.journal_id['outbound_payment_method_ids'] # si hay cuenta en diario y son iguales, y si los metodos de pago # y cobro son solamente uno, usamos el del diario, si no, usamos el # de la compañía if credit_account and credit_account == debit_account and len( inbound_methods) == 1 and len(outbound_methods) == 1: account |= credit_account else: account |= rec.company_id._get_check_account('holding') if len(account) != 1: raise ValidationError(_('Error not specified')) return account @api.model def _get_checks_to_date_on_state(self, state, date, force_domain=None): """ Devuelve el listado de cheques que a la fecha definida se encontraban en el estadao definido. Esta función no la usamos en este módulo pero si en otros que lo extienden La funcion devuelve un listado de las operaciones a traves de las cuales se puede acceder al cheque, devolvemos las operaciones porque dan información util de fecha, partner y demas """ # buscamos operaciones anteriores a la fecha que definan este estado if not force_domain: force_domain = [] operations = self.operation_ids.search( [('date', '<=', date), ('operation', '=', state)] + force_domain) for operation in operations: # buscamos si hay alguna otra operacion posterior para el cheque newer_op = operation.search([ ('date', '<=', date), ('id', '>', operation.id), ('check_id', '=', operation.check_id.id), ]) # si hay una operacion posterior borramos la op del cheque porque # hubo otra operación antes de la fecha if newer_op: operations -= operation return operations @api.multi def _get_operation(self, operation, partner_required=False): self.ensure_one() op = self.operation_ids.search([('check_id', '=', self.id), ('operation', '=', operation)], limit=1) if partner_required: if not op.partner_id: raise ValidationError( _('The %s (id %s) operation has no partner linked.' 'You will need to do it manually.') % (operation, op.id)) return op @api.multi def claim(self): self.ensure_one() if self.state in ['rejected'] and self.type == 'third_check': # anulamos la operación en la que lo recibimos operation = self._get_operation('holding', True) return self.action_create_debit_note( 'reclaimed', 'customer', operation.partner_id, self.company_id._get_check_account('rejected')) @api.multi def customer_return(self): self.ensure_one() if self.state in ['holding'] and self.type == 'third_check': operation = self._get_operation('holding', True) return self.action_create_debit_note( 'returned', 'customer', operation.partner_id, self.get_third_check_account()) @api.multi def reject(self): self.ensure_one() if self.state in ['deposited', 'selled']: operation = self._get_operation(self.state) if operation.origin._name == 'account.payment': journal = operation.origin.destination_journal_id # for compatibility with migration from v8 elif operation.origin._name == 'account.move': journal = operation.origin.journal_id else: raise ValidationError( _('The deposit operation is not linked to a payment.' 'If you want to reject you need to do it manually.')) vals = self.get_bank_vals('bank_reject', journal) action_date = self._context.get('action_date') vals['date'] = action_date move = self.env['account.move'].create(vals) move.post() self._add_operation('rejected', move, date=action_date) elif self.state == 'delivered': operation = self._get_operation(self.state, True) return self.action_create_debit_note( 'rejected', 'supplier', operation.partner_id, self.company_id._get_check_account('rejected')) elif self.state == 'handed': operation = self._get_operation(self.state, True) return self.action_create_debit_note( 'rejected', 'supplier', operation.partner_id, self.company_id._get_check_account('deferred')) @api.multi def action_create_debit_note(self, operation, partner_type, partner, account): self.ensure_one() action_date = self._context.get('action_date') if partner_type == 'supplier': invoice_type = 'in_invoice' journal_type = 'purchase' view_id = self.env.ref('account.invoice_supplier_form').id else: invoice_type = 'out_invoice' journal_type = 'sale' view_id = self.env.ref('account.invoice_form').id journal = self.env['account.journal'].search([ ('company_id', '=', self.company_id.id), ('type', '=', journal_type), ], limit=1) # si pedimos rejected o reclamo, devolvemos mensaje de rechazo y cuenta # de rechazo if operation in ['rejected', 'reclaimed']: name = 'Rechazo cheque "%s"' % (self.name) # si pedimos la de holding es una devolucion elif operation == 'returned': name = 'Devolución cheque "%s"' % (self.name) else: raise ValidationError( _('Debit note for operation %s not implemented!' % (operation))) inv_line_vals = { # 'product_id': self.product_id.id, 'name': name, 'account_id': account.id, 'price_unit': (self.amount_currency and self.amount_currency or self.amount), # 'invoice_id': invoice.id, } inv_vals = { # this is the reference that goes on account.move.line of debt line # 'name': name, # this is the reference that goes on account.move 'rejected_check_id': self.id, 'reference': name, 'date_invoice': action_date, 'origin': _('Check nbr (id): %s (%s)') % (self.name, self.id), 'journal_id': journal.id, # this is done on muticompany fix # 'company_id': journal.company_id.id, 'partner_id': partner.id, 'type': invoice_type, 'invoice_line_ids': [(0, 0, inv_line_vals)], } if self.currency_id: inv_vals['currency_id'] = self.currency_id.id # we send internal_type for compatibility with account_document invoice = self.env['account.invoice'].with_context( internal_type='debit_note').create(inv_vals) self._add_operation(operation, invoice, partner, date=action_date) return { 'name': name, 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.invoice', 'view_id': view_id, 'res_id': invoice.id, 'type': 'ir.actions.act_window', } @api.multi def get_bank_vals(self, action, journal): self.ensure_one() # TODO improove how we get vals, get them in other functions if action == 'bank_debit': # self.journal_id.default_debit_account_id.id, al debitar # tenemos que usar esa misma credit_account = journal.default_debit_account_id # la contrapartida es la cuenta que reemplazamos en el pago debit_account = self.company_id._get_check_account('deferred') name = _('Check "%s" debit') % (self.name) elif action == 'bank_reject': # al transferir a un banco se usa esta. al volver tiene que volver # por la opuesta # self.destination_journal_id.default_credit_account_id credit_account = journal.default_debit_account_id debit_account = self.company_id._get_check_account('rejected') name = _('Check "%s" rejection') % (self.name) else: raise ValidationError( _('Action %s not implemented for checks!') % action) debit_line_vals = { 'name': name, 'account_id': debit_account.id, # 'partner_id': partner, 'debit': self.amount, 'amount_currency': self.amount_currency, 'currency_id': self.currency_id.id, # 'ref': ref, } credit_line_vals = { 'name': name, 'account_id': credit_account.id, # 'partner_id': partner, 'credit': self.amount, 'amount_currency': self.amount_currency, 'currency_id': self.currency_id.id, # 'ref': ref, } return { 'ref': name, 'journal_id': journal.id, 'date': fields.Date.today(), 'line_ids': [(0, False, debit_line_vals), (0, False, credit_line_vals)], }
class PurchaseOrder(models.Model): _inherit = 'purchase.order' _description = 'Purchase Order' @api.depends('order_line.price_total') def _amount_all(self): super(PurchaseOrder, self)._amount_all() for order in self: amount_discount = 0.0 for line in order.order_line: amount_discount += ((line.discount / 100) * line.price_subtotal) amount_total = order.amount_untaxed + order.amount_tax - \ amount_discount order.update({ 'amount_discount': order.currency_id.round(amount_discount), 'amount_total': order.currency_id.round(amount_total), }) amount_untaxed = fields.Monetary(string='Subtotal') amount_discount = fields.Monetary(compute='_amount_all', string='Discount', store=True) @api.cr_uid_id_context def action_invoice_create(self, cr, uid, ids, context=None): """ You have to make a super original method and update invoice lines with the discount that lines has on the purchase order. You can not directly update the discount because the discount is calculated on the invoice """ account_invoice_line_obj = self.pool.get('account.invoice.line') res = super(PurchaseOrder, self).action_invoice_create(cr, uid, ids, context=context) invoice_lines_ids = account_invoice_line_obj.search( cr, uid, [('invoice_id', '=', res)], context=context) invoice_lines = account_invoice_line_obj.browse(cr, uid, invoice_lines_ids, context=context) for purchase in self.browse(cr, uid, ids, context=context): """zip is a function that enables iterating through two lists simultaneously. for a,b in (list_a,list_b), where a is iterator for list_a and b is iterator for list_b zip know if any of list is empty and stop the iteration""" """In this for, iterates in both list and extract the discount for purchase line and update the invoice line with the id (invoice_line.id)""" for purchase_line, invoice_line in zip(purchase.order_line, invoice_lines): if purchase_line.discount is not None: discount = purchase_line.discount account_invoice_line_obj.write(cr, uid, [invoice_line.id], {'discount': discount}, context=context) return res
class LunchOrder(models.Model): """ A lunch order contains one or more lunch order line(s). It is associated to a user for a given date. When creating a lunch order, applicable lunch alerts are displayed. """ _name = 'lunch.order' _description = 'Lunch Order' _order = 'date desc' def _default_previous_order_ids(self): prev_order = self.env['lunch.order.line'].search( [('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc') # If we return return prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. return {(order.product_id, order.note): order.id for order in prev_order}.values() user_id = fields.Many2one('res.users', 'User', required=True, readonly=True, states={'new': [('readonly', False)]}, default=lambda self: self.env.uid) date = fields.Date('Date', required=True, readonly=True, states={'new': [('readonly', False)]}, default=fields.Date.context_today) order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products', ondelete="cascade", readonly=True, copy=True, states={ 'new': [('readonly', False)], False: [('readonly', False)] }) total = fields.Float(compute='_compute_total', string="Total", store=True) state = fields.Selection([('new', 'New'), ('confirmed', 'Received'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, copy=False, default='new', compute='_compute_order_state', store=True) alerts = fields.Text(compute='_compute_alerts_get', string="Alerts") previous_order_ids = fields.Many2many( 'lunch.order.line', compute='_compute_previous_order_ids', default=lambda self: self._default_previous_order_ids()) company_id = fields.Many2one('res.company', related='user_id.company_id', store=True) currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True) cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance') balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance') @api.one @api.depends('order_line_ids') def _compute_total(self): """ get and sum the order lines' price """ self.total = sum(orderline.price for orderline in self.order_line_ids) @api.multi def name_get(self): return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self] @api.depends('state') def _compute_alerts_get(self): """ get the alerts to display on the order form """ alert_msg = [ alert.message for alert in self.env['lunch.alert'].search([]) if alert.display ] if self.state == 'new': self.alerts = alert_msg and '\n'.join(alert_msg) or False @api.depends('user_id') def _compute_previous_order_ids(self): self.previous_order_ids = self._default_previous_order_ids() @api.one @api.depends('user_id') def _compute_cash_move_balance(self): domain = [('user_id', '=', self.user_id.id)] lunch_cash = self.env['lunch.cashmove'].read_group( domain, ['amount', 'user_id'], ['user_id']) if len(lunch_cash): self.cash_move_balance = lunch_cash[0]['amount'] self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups( 'lunch.group_lunch_manager') @api.one @api.constrains('date') def _check_date(self): """ Prevents the user to create an order in the past """ date_order = datetime.datetime.strptime(self.date, '%Y-%m-%d') date_today = datetime.datetime.strptime( fields.Date.context_today(self), '%Y-%m-%d') if (date_order < date_today): raise UserError(_('The date of your order is in the past.')) @api.one @api.depends('order_line_ids.state') def _compute_order_state(self): """ Update the state of lunch.order based on its orderlines. Here is the logic: - if at least one order line is cancelled, the order is set as cancelled - if no line is cancelled but at least one line is not confirmed, the order is set as new - if all lines are confirmed, the order is set as confirmed """ if not self.order_line_ids: self.state = 'new' else: isConfirmed = True for orderline in self.order_line_ids: if orderline.state == 'cancelled': self.state = 'cancelled' return elif orderline.state == 'confirmed': continue else: isConfirmed = False if isConfirmed: self.state = 'confirmed' else: self.state = 'new' return
class AccountPayment(models.Model): _inherit = 'account.payment' # _name = 'account.check' # _description = 'Account Check' # _order = "id desc" # _inherit = ['mail.thread'] # communication = fields.Char( # # because onchange function is not called on onchange and we want # # to clean check number name # copy=False, # ) # TODO tal vez renombrar a check_ids check_ids = fields.Many2many( 'account.check', # 'account.move.line', # 'check_deposit_id', string='Checks', copy=False, readonly=True, states={'draft': [('readonly', '=', False)]} ) # only for v8 comatibility where more than one check could be received # or issued check_ids_copy = fields.Many2many( related='check_ids', readonly=True, ) readonly_currency_id = fields.Many2one( related='currency_id', readonly=True, ) readonly_amount = fields.Monetary( # string='Payment Amount', # required=True related='amount', readonly=True, ) # we add this field for better usability on issue checks and received # checks. We keep m2m field for backward compatibility where we allow to # use more than one check per payment check_id = fields.Many2one( 'account.check', compute='_compute_check', string='Check', # string='Payment Amount', # required=True ) @api.multi @api.depends('check_ids') def _compute_check(self): for rec in self: # we only show checks for issue checks or received thid checks # if len of checks is 1 if rec.payment_method_code in ( 'received_third_check', 'issue_check',) and len(rec.check_ids) == 1: rec.check_id = rec.check_ids[0].id # check fields, just to make it easy to load checks without need to create # them by a m2o record check_name = fields.Char( 'Check Name', # required=True, readonly=True, copy=False, states={'draft': [('readonly', False)]}, ) check_number = fields.Integer( 'Check Number', # required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False ) check_issue_date = fields.Date( 'Check Issue Date', # required=True, readonly=True, copy=False, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, ) check_payment_date = fields.Date( 'Check Payment Date', readonly=True, help="Only if this check is post dated", states={'draft': [('readonly', False)]} ) checkbook_id = fields.Many2one( 'account.checkbook', 'Checkbook', readonly=True, states={'draft': [('readonly', False)]}, # TODO hacer con un onchange # default=_get_checkbook, ) check_subtype = fields.Selection( related='checkbook_id.issue_check_subtype', ) check_bank_id = fields.Many2one( 'res.bank', 'Check Bank', readonly=True, copy=False, states={'draft': [('readonly', False)]} ) check_owner_vat = fields.Char( # TODO rename to Owner VAT 'Check Owner Vat', readonly=True, copy=False, states={'draft': [('readonly', False)]} ) check_owner_name = fields.Char( 'Check Owner Name', readonly=True, copy=False, states={'draft': [('readonly', False)]} ) check_type = fields.Char( compute='_compute_check_type', # this fields is to help with code and view ) @api.multi @api.depends('payment_method_code') def _compute_check_type(self): for rec in self: if rec.payment_method_code == 'issue_check': rec.check_type = 'issue_check' elif rec.payment_method_code in [ 'received_third_check', 'delivered_third_check']: rec.check_type = 'third_check' # on change methods # @api.constrains('check_ids') @api.onchange('check_ids', 'payment_method_code') def onchange_checks(self): # we only overwrite if payment method is delivered if self.payment_method_code == 'delivered_third_check': self.amount = sum(self.check_ids.mapped('amount')) # TODo activar @api.one @api.onchange('check_number', 'checkbook_id') def change_check_number(self): # TODO make default padding a parameter if self.payment_method_code in ['received_third_check']: if not self.check_number: check_name = False else: # TODO make optional padding = 8 if len(str(self.check_number)) > padding: padding = len(str(self.check_number)) # communication = _('Check nbr %s') % ( check_name = ('%%0%sd' % padding % self.check_number) # communication = ( # '%%0%sd' % padding % self.check_number) self.check_name = check_name @api.onchange('check_issue_date', 'check_payment_date') def onchange_date(self): if ( self.check_issue_date and self.check_payment_date and self.check_issue_date > self.check_payment_date): self.check_payment_date = False raise UserError( _('Check Payment Date must be greater than Issue Date')) @api.one @api.onchange('partner_id') def onchange_partner_check(self): commercial_partner = self.partner_id.commercial_partner_id self.check_bank_id = ( commercial_partner.bank_ids and commercial_partner.bank_ids[0].bank_id.id or False) self.check_owner_name = commercial_partner.name # TODO use document number instead of vat? self.check_owner_vat = commercial_partner.vat @api.onchange('payment_method_code') def _onchange_payment_method_code(self): if self.payment_method_code == 'issue_check': checkbook = self.env['account.checkbook'].search([ ('state', '=', 'active'), ('journal_id', '=', self.journal_id.id)], limit=1) self.checkbook_id = checkbook @api.onchange('checkbook_id') def onchange_checkbook(self): if self.checkbook_id: self.check_number = self.checkbook_id.next_number # post methods @api.model def create(self, vals): issue_checks = self.env.ref( 'account_check.account_payment_method_issue_check') if vals['payment_method_id'] == issue_checks.id and vals.get( 'checkbook_id'): checkbook = self.env['account.checkbook'].browse( vals['checkbook_id']) vals.update({ # beacause number was readonly we write it here 'check_number': checkbook.next_number, 'check_name': checkbook.sequence_id.next_by_id(), }) return super(AccountPayment, self.sudo()).create(vals) @api.multi def cancel(self): res = super(AccountPayment, self).cancel() for rec in self: rec.do_checks_operations(cancel=True) # if rec.check_id: # # rec.check_id._add_operation('cancel') # rec.check_id._del_operation() # rec.check_id.unlink() # elif rec.check_ids: # rec.check_ids._del_operation() return res @api.multi def create_check(self, check_type, operation, bank): self.ensure_one() check_vals = { 'bank_id': bank.id, 'owner_name': self.check_owner_name, 'owner_vat': self.check_owner_vat, 'number': self.check_number, 'name': self.check_name, 'checkbook_id': self.checkbook_id.id, 'issue_date': self.check_issue_date, 'type': self.check_type, 'journal_id': self.journal_id.id, 'amount': self.amount, 'payment_date': self.check_payment_date, # TODO arreglar que monto va de amount y cual de amount currency # 'amount_currency': self.amount, 'currency_id': self.currency_id.id, } check = self.env['account.check'].create(check_vals) self.check_ids = [(4, check.id, False)] check._add_operation(operation, self, self.partner_id) return check @api.multi def get_third_check_account(self): """ For third checks, if we use a journal only for third checks, we use accounts on journal, if not we use company account """ self.ensure_one() if self.payment_type in ('outbound', 'transfer'): account = self.journal_id.default_debit_account_id methods_field = 'outbound_payment_method_ids' else: account = self.journal_id.default_credit_account_id methods_field = 'inbound_payment_method_ids' if len(self.journal_id[methods_field]) > 1 or not account: account = self.company_id._get_check_account('holding') return account @api.multi def do_checks_operations(self, vals=None, cancel=False): """ Check attached .ods file on this module to understand checks workflows This method is called from: * cancellation of payment to execute delete the right operation and unlink check if needed * from _get_liquidity_move_line_vals to add check operation and, if needded, change payment vals and/or create check and """ self.ensure_one() rec = self if not rec.check_type: # continue return vals if ( rec.payment_method_code == 'received_third_check' and rec.payment_type == 'inbound' and rec.partner_type == 'customer'): operation = 'holding' if cancel: _logger.info('Cancel Receive Check') rec.check_ids._del_operation(operation) rec.check_ids.unlink() return None _logger.info('Receive Check') self.create_check('third_check', operation, self.check_bank_id) vals['date_maturity'] = self.check_payment_date vals['account_id'] = self.get_third_check_account().id elif ( rec.payment_method_code == 'delivered_third_check' and rec.payment_type == 'transfer' and rec.destination_journal_id.type == 'cash'): operation = 'selled' if cancel: _logger.info('Cancel Sell Check') rec.check_ids._del_operation(operation) return None _logger.info('Sell Check') rec.check_ids._add_operation( operation, rec, False) vals['account_id'] = self.get_third_check_account().id elif ( rec.payment_method_code == 'delivered_third_check' and rec.payment_type == 'transfer' and rec.destination_journal_id.type == 'bank'): operation = 'deposited' if cancel: _logger.info('Cancel Deposit Check') rec.check_ids._del_operation(operation) return None _logger.info('Deposit Check') rec.check_ids._add_operation( operation, rec, False) vals['account_id'] = self.get_third_check_account().id elif ( rec.payment_method_code == 'delivered_third_check' and rec.payment_type == 'outbound' and rec.partner_type == 'supplier'): operation = 'delivered' if cancel: _logger.info('Cancel Deliver Check') rec.check_ids._del_operation(operation) return None _logger.info('Deliver Check') rec.check_ids._add_operation( operation, rec, rec.partner_id) vals['account_id'] = self.get_third_check_account().id elif ( rec.payment_method_code == 'issue_check' and rec.payment_type == 'outbound' and rec.partner_type == 'supplier'): operation = 'handed' if cancel: _logger.info('Cancel Hand Check') rec.check_ids._del_operation(operation) rec.check_ids.unlink() return None _logger.info('Hand Check') self.create_check('issue_check', operation, self.check_bank_id) vals['date_maturity'] = self.check_payment_date # if check is deferred, change account if self.check_subtype == 'deferred': vals['account_id'] = self.company_id._get_check_account( 'deferred').id elif ( rec.payment_method_code == 'issue_check' and rec.payment_type == 'transfer' and rec.destination_journal_id.type == 'cash'): operation = 'withdrawed' if cancel: _logger.info('Cancel Withdrawal Check') rec.check_ids._del_operation(operation) rec.check_ids.unlink() return None _logger.info('Hand Check') self.create_check('issue_check', operation, self.check_bank_id) vals['date_maturity'] = self.check_payment_date # if check is deferred, change account # si retiramos por caja directamente lo sacamos de banco # if self.check_subtype == 'deferred': # vals['account_id'] = self.company_id._get_check_account( # 'deferred').id else: raise UserError(_( 'This operatios is not implemented for checks:\n' '* Payment type: %s\n' '* Partner type: %s\n' '* Payment method: %s\n' '* Destination journal: %s\n' % ( rec.payment_type, rec.partner_type, rec.payment_method_code, rec.destination_journal_id.type))) return vals # @api.multi # def post(self): # self.do_checks_operations() # return super(AccountPayment, self).post() def _get_liquidity_move_line_vals(self, amount): vals = super(AccountPayment, self)._get_liquidity_move_line_vals( amount) vals = self.do_checks_operations(vals=vals) # if self.check_type: # vals['date_maturity'] = self.check_payment_date # if self.check_subtype == 'deferred': # deferred_account = self.company_id.deferred_check_account_id # if not deferred_account: # raise UserError(_( # 'No checks deferred account defined for company %s' # ) % self.company_id.name) # vals['account_id'] = deferred_account.id # # vals['check_bank_id'] = self.check_bank_id.id # # vals['check_owner_name'] = self.check_owner_name # # vals['check_owner_vat'] = self.check_owner_vat # # vals['check_number'] = self.check_number # # vals['checkbook_id'] = self.checkbook_id.id # # vals['check_issue_date'] = self.check_issue_date # # if self.payment_method_code == 'issue_check': # # vals['check_type'] = 'issue_check' # # else: # # vals['check_type'] = 'third_check' return vals
class AccountDebtLine(models.Model): _name = "account.debt.line" _description = "Account Debt Line" _auto = False _rec_name = 'document_number' _order = 'date asc, date_maturity asc, document_number asc, id' _depends = { 'res.partner': [ 'user_id', ], 'account.move': [ 'document_type_id', 'document_number', ], 'account.move.line': [ 'account_id', 'debit', 'credit', 'date_maturity', 'partner_id', 'amount_currency', ], } blocked = fields.Boolean( string='No Follow-up', readonly=True, help="You can check this box to mark this journal item as a " "litigation with the associated partner" ) document_type_id = fields.Many2one( 'account.document.type', 'Document Type', readonly=True ) document_number = fields.Char( readonly=True, string='Document Number', ) date = fields.Date( readonly=True ) date_maturity = fields.Date( readonly=True ) ref = fields.Char( 'Reference', readonly=True ) amount = fields.Monetary( readonly=True, currency_field='company_currency_id', ) amount_residual = fields.Monetary( readonly=True, string='Residual Amount', currency_field='company_currency_id', ) currency_id = fields.Many2one( 'res.currency', 'Currency', readonly=True ) amount_currency = fields.Monetary( readonly=True, currency_field='currency_id', ) amount_residual_currency = fields.Monetary( readonly=True, string='Residual Amount in Currency', currency_field='currency_id', ) move_lines_str = fields.Char( 'Entry Lines String', readonly=True ) account_id = fields.Many2one( 'account.account', 'Account', readonly=True ) internal_type = fields.Selection([ ('receivable', 'Receivable'), ('payable', 'Payable')], 'Type', readonly=True, ) # move_state = fields.Selection( # [('draft', 'Unposted'), ('posted', 'Posted')], # 'Status', # readonly=True # ) reconciled = fields.Boolean( ) partner_id = fields.Many2one( 'res.partner', 'Partner', readonly=True ) account_type = fields.Many2one( 'account.account.type', 'Account Type', readonly=True ) company_id = fields.Many2one( 'res.company', 'Company', readonly=True ) # computed fields financial_amount = fields.Monetary( compute='_compute_move_lines_data', currency_field='company_currency_id', ) financial_amount_residual = fields.Monetary( compute='_compute_move_lines_data', currency_field='company_currency_id', ) # we get this line to make it easier to compute other lines # for debt lines, as we group by due date, we should have only one move_line_id = fields.Many2one( 'account.move.line', string='Entry line', compute='_compute_move_lines_data', ) move_line_ids = fields.One2many( 'account.move.line', string='Entry lines', compute='_compute_move_lines_data', ) move_id = fields.Many2one( 'account.move', string='Entry', compute='_compute_move_lines_data', ) move_ids = fields.One2many( 'account.move', string='Entries', compute='_compute_move_lines_data', ) company_currency_id = fields.Many2one( related='company_id.currency_id', readonly=True, ) payment_group_id = fields.Many2one( 'account.payment.group', 'Payment Group', compute='_compute_move_lines_data', ) invoice_id = fields.Many2one( 'account.invoice', 'Invoice', compute='_compute_move_lines_data', ) # es una concatenacion de los name de los move lines name = fields.Char( compute='_compute_move_lines_data', ) statement_id = fields.Many2one( 'account.bank.statement', 'Statement', compute='_compute_move_lines_data', ) # TODO por ahora, y si nadie lo extraña, vamos a usar document_number # en vez de este, alternativas por si se extraña: # si se extraña entonces tal vez mejor restaurarlo con otro nombre # @api.one # def get_display_name(self): # # usamos display_name para que contenga doc number o name # # luego si el ref es igual al name del move no lo mostramos # display_name = self.move_id.display_name # ref = False # # because account voucher replace / with '' # move_names = [self.move_id.name, self.move_id.name.replace('/', '')] # # solo agregamos el ref del asiento o el name del line si son # # distintos a el name del asiento # if self.ref and self.ref not in move_names: # ref = self.ref # elif ( # self.move_line_id.name and # self.move_line_id.name != '/' and # self.move_line_id.name not in move_names): # ref = self.move_line_id.name # if ref: # display_name = '%s (%s)' % (display_name, ref) # self.display_name = display_name @api.multi @api.depends('move_lines_str') # @api.depends('amount', 'amount_currency') def _compute_move_lines_data(self): """ If debt_together in context then we discount payables and make cumulative all together """ for rec in self: move_lines = rec.move_line_ids.browse( literal_eval(rec.move_lines_str)) rec.move_line_ids = move_lines rec.name = ', '.join(move_lines.mapped('name')) rec.move_ids = rec.move_line_ids.mapped('move_id') if len(rec.move_ids) == 1: rec.move_id = rec.move_ids if len(move_lines) == 1: # return one line or empty recordset rec.move_line_id = ( len(move_lines) == 1 and move_lines[0] or rec.env['account.move.line']) rec.invoice_id = rec.move_line_id.invoice_id rec.payment_group_id = rec.move_line_id.mapped( 'payment_id.payment_group_id') rec.statement_id = rec.move_line_id.statement_id # invoices = rec.move_line_ids.mapped('invoice_id') # if len(invoices) == 1: # rec.invoice_id = invoices # payment_groups = rec.move_line_ids.mapped( # 'payment_id.payment_group_id') # if len(payment_groups) == 1: # rec.payment_group_id = payment_groups # statements = rec.move_line_ids.mapped('statement_id') # if len(statements) == 1: # rec.statement_id = statements rec.financial_amount = sum( rec.move_line_ids.mapped('financial_amount')) rec.financial_amount_residual = sum( rec.move_line_ids.mapped('financial_amount_residual')) def init(self, cr): tools.drop_view_if_exists(cr, self._table) date_maturity_type = self.pool['ir.config_parameter'].get_param( cr, 1, 'account_debt_management.date_maturity_type') if date_maturity_type == 'detail': params = ('l.date_maturity as date_maturity,', ', l.date_maturity') elif date_maturity_type == 'max': params = ('max(l.date_maturity) as date_maturity,', '') else: params = ('min(l.date_maturity) as date_maturity,', '') query = """ SELECT -- es una funcion y se renumera constantemente, por eso -- necesita el over -- ROW_NUMBER() OVER (ORDER BY l.partner_id, am.company_id, -- l.account_id, l.currency_id, a.internal_type, -- a.user_type_id, c.document_number, am.document_type_id, -- l.date_maturity) as id, -- igualmente los move lines son unicos, usamos eso como id max(l.id) as id, string_agg(cast(l.id as varchar), ',') as move_lines_str, max(am.date) as date, %s am.document_type_id as document_type_id, c.document_number as document_number, bool_and(l.reconciled) as reconciled, -- l.blocked as blocked, -- si cualquier deuda esta bloqueada de un comprobante, -- toda deberia estar bloqueda bool_and(l.blocked) as blocked, -- TODO borrar, al final no pudimos hacerlo asi porque si no -- agrupamos por am.name, entonces todo lo que no tenga tipo -- de doc lo muestra en una linea. Y si lo agregamos nos quedan -- desagregados los multiples pagos (y otros similares) -- si devuelve '' el concat del prefix y number lo cambiamos -- por null y luego coalesce se encarga de elerig el name -- devolvemos el string_agg de am.name para no tener que -- agregarlo en la clausula del group by -- COALESCE(NULLIF(CONCAT( -- dt.doc_code_prefix, am.document_number), ''), -- string_agg(am.name, ',')) as document_number, string_agg(am.ref, ',') as ref, --am.state as move_state, --l.full_reconcile_id as full_reconcile_id, --l.reconciled as reconciled, -- l.reconcile_partial_id as reconcile_partial_id, l.partner_id as partner_id, am.company_id as company_id, a.internal_type as internal_type, -- am.journal_id as journal_id, -- p.fiscalyear_id as fiscalyear_id, -- am.period_id as period_id, l.account_id as account_id, --l.analytic_account_id as analytic_account_id, -- a.internal_type as type, a.user_type_id as account_type, l.currency_id as currency_id, sum(l.amount_currency) as amount_currency, sum(l.amount_residual_currency) as amount_residual_currency, sum(l.amount_residual) as amount_residual, --pa.user_id as user_id, sum(l.balance) as amount -- coalesce(l.debit, 0.0) - coalesce(l.credit, 0.0) as amount FROM account_move_line l left join account_account a on (l.account_id = a.id) left join account_move am on (am.id=l.move_id) -- left join account_period p on (am.period_id=p.id) -- left join res_partner pa on (l.partner_id=pa.id) left join account_document_type dt on ( am.document_type_id=dt.id) left join ( SELECT COALESCE (NULLIF (CONCAT ( dt.doc_code_prefix, am.document_number), ''), am.name) as document_number, am.id FROM account_move am left join account_document_type dt on ( am.document_type_id=dt.id) ) c on l.move_id = c.id WHERE -- l.state != 'draft' and a.internal_type IN ('payable', 'receivable') GROUP BY l.partner_id, am.company_id, l.account_id, l.currency_id, a.internal_type, a.user_type_id, c.document_number, am.document_type_id %s -- dt.doc_code_prefix, am.document_number """ % params cr.execute("""CREATE or REPLACE VIEW %s as (%s )""" % (self._table, query)) @api.multi def action_open_related_document(self): self.ensure_one() # usamos lo que ya se usa en js para devolver la accion res_model, res_id, action_name, view_id = self.get_model_id_and_name() return { 'type': 'ir.actions.act_window', 'name': action_name, 'res_model': res_model, 'view_type': 'form', 'view_mode': 'form', 'views': [[view_id, 'form']], 'res_id': res_id, # 'view_id': res[0], } @api.multi def get_model_id_and_name(self): """ Function used to display the right action on journal items on dropdown lists, in reports like general ledger """ if self.statement_id: return [ 'account.bank.statement', self.statement_id.id, _('View Bank Statement'), False] if self.payment_group_id: return [ 'account.payment.group', self.payment_group_id.id, _('View Payment Group'), False] if self.invoice_id: view_id = self.invoice_id.get_formview_id() return [ 'account.invoice', self.invoice_id.id, _('View Invoice'), view_id] # TODO ver si implementamos que pasa cuando hay mas de un move return [ 'account.move', self.move_id.id, _('View Move'), False]
class PaymentRequest(models.Model): _name = 'payment.request' _description = 'Payment Request' _inherit = ['mail.thread', 'ir.needaction_mixin'] @api.model def _company_get(self): company_id = self.env['res.company']._company_default_get(self._name) return self.env['res.company'].browse(company_id.id) @api.model def _default_currency(self): company = self.env['res.company']._company_default_get('payment.request') return company.currency_id @api.model def _get_default_requested_by(self): return self.env['res.users'].browse(self.env.uid) @api.model @api.depends('requested_by') def _get_default_employee(self): requested_by = self.requested_by.id #self.employee_id = self.env.cr.execute("SELECT hr_employee.id FROM hr_employee inner join resource_resource on hr_employee.resource_id = resource_resource.id where user_id ='%s'" %(self.requested_by.id)) res = self.env.cr.fetchone()[0] self.employee_id = res @api.model def _get_default_name(self): return self.env['ir.sequence'].get('payment.request') @api.model def _get_default_origin(self): for rec in self: rec.origin = rec.name @api.model def _default_picking_type(self): type_obj = self.env['stock.picking.type'] company_id = self.env.context.get('company_id') or \ self.env.user.company_id.id types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)]) if not types: types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)]) return types[:1] @api.multi @api.depends('state') def _compute_is_editable(self): for rec in self: if rec.state in (2,3,4,5,7): rec.is_editable = False else: rec.is_editable = True @api.multi def _track_subtype(self, init_values): for rec in self: if 'state' in init_values and rec.state == 2: return 'purchase_request.mt_request_to_approve' elif 'state' in init_values and rec.state == 3: return 'purchase_request.mt_request_to_department_manager_approved' elif 'state' in init_values and rec.state == 4: return 'purchase_request.mt_request_to_accountant_manager_approved' elif 'state' in init_values and rec.state == 5: return 'purchase_request.mt_request_approved' elif 'state' in init_values and rec.state == 7: return 'purchase_request.mt_request_rejected' return super(PaymentRequest, self)._track_subtype(init_values) @api.onchange('state', 'partner_id') def _onchange_allowed_purchase_ids(self): ''' The purpose of the method is to define a domain for the available purchase orders. ''' result = {} # A PO can be selected only if at least one PO line is not already in the invoice purchase_line_ids = self.line_ids.mapped('purchase_line_id') purchase_ids = self.line_ids.mapped('purchase_id').filtered(lambda r: r.order_line <= purchase_line_ids) result['domain'] = {'purchase_id': [ ('invoice_status', '=', 'to invoice'), ('partner_id', 'child_of', self.partner_id.id), ('id', 'not in', purchase_ids.ids), ]} return result def _prepare_invoice_line_from_po_line(self, line): if line.product_id.purchase_method == 'purchase': qty = line.product_qty - line.qty_invoiced else: qty = line.qty_received - line.qty_invoiced if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0: qty = 0.0 taxes = line.taxes_id invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes) invoice_line = self.env['payment.request.line'] data = { 'purchase_line_id': line.id, 'name': line.name, 'origin': self.purchase_id.origin, 'uom_id': line.product_uom.id, 'product_id': line.product_id.id, #'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(), #'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False), 'product_price': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False), #'quantity': qty, 'product_qty': line.product_qty, 'discount': 0.0, 'account_analytic_id': line.account_analytic_id.id, 'invoice_line_tax_ids': invoice_line_tax_ids.ids } #account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id) account = 0 if account: data['account_id'] = account.id return data # Load all unsold PO lines @api.onchange('purchase_id') def purchase_order_change(self): if not self.purchase_id: return {} if not self.partner_id: self.partner_id = self.purchase_id.partner_id.id new_lines = self.env['payment.request.line'] for line in self.purchase_id.order_line - self.line_ids.mapped('purchase_line_id'): data = self._prepare_invoice_line_from_po_line(line) new_line = new_lines.new(data) new_line._set_additional_fields(self) new_lines += new_line self.line_ids += new_lines self.purchase_id = False return {} name = fields.Char('Payment Reference', size=32, required=True, default=_get_default_name,track_visibility='onchange') purchase_id = fields.Many2one('purchase.order', string='Add Purchase Order', help='Encoding help. When selected, the associated purchase order lines are added to the vendor bill. Several PO can be selected.') partner_id = fields.Many2one('res.partner', string='Supplier Name', change_default=True, required=True, track_visibility='always') origin = fields.Char('Source Document', size=32, compute=_get_default_origin) reject_reason = fields.Char('reject_reason', default=' ') date_start = fields.Date('Creation date', help="Date when the user initiated the request.", default=fields.Date.context_today, track_visibility='onchange') requested_by = fields.Many2one('res.users','Requestor',required=True, track_visibility='onchange', default=_get_default_requested_by) employee_id = fields.Many2one('hr.employee', string='Staff ID', compute="_get_default_employee") employee_position = fields.Many2one('hr.job', string='Position', related='employee_id.job_id') assigned_to = fields.Many2one('res.users', 'Head of Department', domain=[('x_department_manager', '=', True)], track_visibility='onchange') assigned_to_2 = fields.Many2one('res.users', 'Head of Department', domain=[('x_department_manager', '=', True)], track_visibility='onchange') assigned_to_3 = fields.Many2one('res.users', 'Project Officer', domain=[('x_project_manager', '=', True)], track_visibility='onchange') description = fields.Text('Purpose of Request') direct_manager_notes = fields.Text('Direct manager notes') department_manager_notes = fields.Text('Department manager notes') #purchase_manager_notes = fields.Text('Purchases manager notes') #warehouse_manager_notes = fields.Text('Warehouse manager notes') accountant_manager_notes = fields.Text('CFO notes') executive_manager_notes = fields.Text('Executive manager notes') treasurer_manager_notes = fields.Text('Treasurer notes') budget_holder_notes = fields.Text('Budget Holder notes') company_id = fields.Many2one('res.company', 'Company',required=True, default=_company_get, track_visibility='onchange') line_ids = fields.One2many('payment.request.line', 'request_id','Expense Details', readonly=False, copy=True,track_visibility='onchange') state = fields.Selection(selection=_STATES,string='Status',index=True, track_visibility='onchange',copy=False,default=1) stage = fields.Selection(selection=_STAGES,default=1,string='Stage') request_type = fields.Selection(selection=_TYPES,default=1,string='Request Type', help = 'Provide request as soon as possible :عاجل') type_reason = fields.Text('Reason') explain_type = fields.Char('Explain',compute='_compute_explain') department = fields.Many2one('hr.department', 'Department', track_visibility='onchange') project = fields.Many2one('hr.department', 'Project/Section', track_visibility='onchange') is_editable = fields.Boolean(string="Is editable",compute="_compute_is_editable", readonly=True) #ontime = fields.Integer(string='On Time',compute="change_ontime",readonly=True) #ontime_stage = fields.Integer(string='On Time Stage',compute="change_ontime_stage",readonly=True) #done1= fields.Char(string='Done',compute="change_done_stage",default='early') #progress = fields.Float(string='Progress',compute='change_progress',readonly=True) color = fields.Integer('Color Index', compute="change_colore_on_kanban") currency_id = fields.Many2one('res.currency', string='Currency', required=True, track_visibility='onchange', ondelete='restrict', default=_default_currency) #amount_requested = fields.Float( string='Amount Requested') budget_id = fields.Many2one('crossovered.budget' , string='Budget', required=True) budget_drc = fields.Many2one('account.budget.post', string="DRC", required=True) budget_dea = fields.Many2one('crossovered.budget.lines', string='DEA', required=True) total_dea = fields.Float(related='budget_dea.planned_amount', string="DEA Amount") invoice_id = fields.Many2one('account.invoice', string='Invoice') invoice_partner = fields.Many2one('res.partner',related='invoice_id.partner_id', string="Invoice Partner") invoice_date = fields.Date(related='invoice_id.date_invoice', string="Invoice Date") invoice_amount = fields.Monetary(string="Invoice Total", related='invoice_id.amount_total') cheque = fields.Boolean(string="Cheque") bank_transfer = fields.Boolean(string="Bank Transfer") cash = fields.Boolean(string="Cash") cheque_number = fields.Char(string="Cheque Number") transfer_reference = fields.Char(string="Transfer Reference") #outstand_payment = fields.Boolean(string="Requestor does not already have an outstanding payment") #exceed_maximum = fields.Boolean(string="Advance does not exceed maximum allocation") coding_correct = fields.Boolean(string="Coding correct") authorised_dfa = fields.Boolean(string="Authorised per DFA") payee_correct = fields.Boolean(string="Payee Correct") Procurement_procedures = fields.Boolean(string='Procurement Procedures Followed') good_received = fields.Boolean(string='Good/Services Received') is_direct_notes = fields.Boolean(string="Is Direct Notes", compute="_compute_is_direct_notes", readonly=True) is_dept_notes = fields.Boolean(string="Is Dept Notes", compute="_compute_is_dept_notes", readonly=True) is_account_notes = fields.Boolean(string="Is Accountant Notes", compute="_compute_is_account_notes", readonly=True) is_treasure_notes = fields.Boolean(string="Is Treasure Notes", compute="_compute_is_treasure_notes", readonly=True) is_holder_notes = fields.Boolean(string="Is Holder Notes", compute="_compute_is_pres_notes", readonly=True) is_executive_notes = fields.Boolean(string="Is Executive Notes", compute="_compute_is_executive_notes", readonly=True) is_usr = fields.Boolean(string="Is Requested by User", compute="_compute_is_usr", readonly=True) is_stage = fields.Boolean(string="Is Stage User", compute="_compute_is_stage", readonly=True) is_required = fields.Boolean(string="Is Required User", compute="_compute_is_required", readonly=True) is_dept_approve = fields.Boolean(string="Is Dept Approve", compute="_compute_is_dept_approve", readonly=True) is_reject_required = fields.Boolean(string="Is Reject Required User") is_request_approval = fields.Boolean(string="Is Request_Approval User", compute="_compute_is_request_approval", readonly=True) total_budget = fields.Float(compute='_compute_budget',store=True) total_drc = fields.Float(compute='_compute_drc',store=True) budget_availability = fields.Float(compute='_compute_budget_availability',store=True) drc_availability = fields.Float(compute='_compute_drc_availability',store=True) dea_availability = fields.Float(compute='_compute_dea_availability',store=True) price_alltotal = fields.Float( string='Total Price', compute='_compute_amount_all', store=True) is_draft = fields.Boolean(string="Is Draft", compute="_compute_is_draft",readonly=True) is_assign_direct_Manager = fields.Boolean(string="Is Assign Direct Manager", compute="_compute_is_assign_direct_Manager",readonly=True) is_assign_head_of_department = fields.Boolean(string="Is Assign Direct Manager", compute="_compute_is_assign_head_of_department",readonly=True) is_assign_cfo_manager = fields.Boolean(string="Is Assign CFO Manager", compute="_compute_is_assign_cfo_manager",readonly=True) is_cfo_approved = fields.Boolean(string='CFO Approved', compute='_compute_is_cfo_approved',readonly=True) is_approved = fields.Boolean(string='Approved',compute='_is_approved',readonly=True) is_rejected = fields.Boolean(string='Rejected',compute='_is_rejected',readonly=True) @api.depends('state') def _compute_is_draft(self): for record in self: if record.state == 1: record.is_draft = True @api.depends('state') def _compute_is_assign_direct_Manager(self): for record in self: if record.state == 2: record.is_assign_direct_Manager = True @api.depends('state') def _compute_is_assign_head_of_department(self): for record in self: if record.state == 3: record.is_assign_head_of_department = True @api.depends('state') def _compute_is_assign_cfo_manager(self): for record in self: if record.state == 4: record.is_assign_cfo_manager = True @api.depends('state') def _compute_is_cfo_approved(self): for record in self: if record.state == 5: record.is_cfo_approved = True @api.depends('state') def _is_approved(self): for record in self: if record.state == 6: record.is_approved = True @api.depends('state') def _is_rejected(self): for record in self: if record.state == 7: record.is_rejected = True @api.depends('budget_id') def _compute_budget(self): for record in self: record.total_budget = record.budget_id.total_cost print record.total_budget @api.depends('budget_drc') def _compute_drc(self): for record in self: record.total_drc = record.budget_drc.total_cost print record.total_drc @api.depends('budget_id') def _compute_budget_availability(self): for record in self: record.budget_availability = record.budget_id.total_availability print record.budget_availability @api.depends('budget_drc') def _compute_drc_availability(self): for record in self: record.drc_availability = record.budget_drc.total_availability print record.drc_availability @api.depends('budget_dea') def _compute_dea_availability(self): for record in self: record.dea_availability = record.budget_dea.budget_availability print record.dea_availability @api.multi @api.depends('requested_by') def _compute_is_request_approval(self): for rec in self: #dep_mang = self.env['res.users'].browse(['res.users'].x_department_manager) usr = self.env['res.users'].browse(self.env.uid) if usr == rec.requested_by and rec.state == 1: rec.is_request_approval = True elif usr == rec.requested_by and rec.state != 1: rec.is_request_approval = False elif usr != rec.requested_by and rec.state == 1: rec.is_request_approval = False elif usr != rec.requested_by and rec.state != 1: rec.is_request_approval = False #elif 'res.user.x_department_manager' == True: # rec.is_required = True else: rec.is_request_approval = False @api.multi @api.depends('requested_by') def _compute_is_required(self): for rec in self: #dep_mang = self.env['res.users'].browse(['res.users'].x_department_manager) usr = self.env['res.users'].browse(self.env.uid) if usr in rec.assigned_to : rec.is_required = True #elif 'res.user.x_department_manager' == True: # rec.is_required = True else: rec.is_required = False @api.multi @api.depends('line_ids.price_total') def _compute_amount_all(self): for request in self: for line in request.line_ids: request.price_alltotal += line.price_total @api.multi @api.depends('requested_by') def _compute_is_stage(self): for rec in self: usr = self.env['res.users'].browse(self.env.uid) if usr.has_group('__export__.res_groups_56'): rec.is_stage = True elif usr.has_group('__export__.res_groups_82'): rec.is_stage = True else: if usr.has_group('__export__.res_groups_59'): rec.is_stage = True else: rec.is_stage = False @api.depends('request_type') def _compute_explain(self): for rec in self: if rec.request_type == 2: rec.explain_type = 'Provide request as soon as possible' elif rec.request_type == 3: rec.explain_type = 'For project and time conditions, the request is made exceptionally from the rules of the regulation' else: rec.explain_type = '' @api.multi @api.depends('requested_by') def _compute_is_usr(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_usr = True else: rec.is_usr = False @api.multi @api.depends('requested_by') def _compute_is_direct_notes(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_direct_notes = True else: user = self.env['res.users'].browse(self.env.uid) if user.has_group('__export__.res_groups_62'): rec.is_direct_notes = True else: rec.is_direct_notes= False @api.multi @api.depends('requested_by') def _compute_is_dept_notes(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_dept_notes = True else: user = self.env['res.users'].browse(self.env.uid) if user.has_group('__export__.res_groups_61'): rec.is_dept_notes = True else: rec.is_dept_notes= False @api.multi @api.depends('requested_by') def _compute_is_dept_approve(self): for rec in self: if rec.assigned_to == self.env['res.users'].browse(self.env.uid): rec.is_dept_approve = True else: rec.is_dept_approve = False @api.multi @api.depends('requested_by') def _compute_is_account_notes(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_account_notes = True else: user = self.env['res.users'].browse(self.env.uid) if user.has_group('__export__.res_groups_59'): rec.is_account_notes = True else: rec.is_account_notes= False @api.multi @api.depends('requested_by') def _compute_is_treasure_notes(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_treasure_notes = True else: user = self.env['res.users'].browse(self.env.uid) if user.has_group('__export__.res_groups_63'): rec.is_treasure_notes = True else: rec.is_treasure_notes= False @api.multi @api.depends('requested_by') def _compute_is_pres_notes(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_pres_notes = True else: user = self.env['res.users'].browse(self.env.uid) if user.has_group('__export__.res_groups_65'): rec.is_pres_notes = True else: rec.is_pres_notes= False @api.multi @api.depends('requested_by') def _compute_is_executive_notes(self): for rec in self: if rec.requested_by == self.env['res.users'].browse(self.env.uid): rec.is_executive_notes = True else: user = self.env['res.users'].browse(self.env.uid) if user.has_group('purchase_request.group_purchase_request_manager'): rec.is_executive_notes = True else: rec.is_executive_notes= False @api.multi def copy(self, default=None): default = dict(default or {}) self.ensure_one() default.update({ 'state': '1', 'name': self.env['ir.sequence'].get('payment.request'), }) return super(PaymentRequest, self).copy(default) @api.model def create(self, vals): request = super(PaymentRequest, self).create(vals) if vals.get('assigned_to'): request.message_subscribe_users(user_ids=[request.assigned_to.id]) return request @api.multi def write(self, vals): res = super(PaymentRequest, self).write(vals) for request in self: if vals.get('assigned_to'): self.message_subscribe_users(user_ids=[request.assigned_to.id]) return res @api.multi def button_draft(self): for rec in self: #rec.state = 'draft' #rec.stage = 1 if self.state == 2 and self.direct_manager_notes != False: rec.state = 1 rec.stage = 1 rec.is_reject_required = False #raise ValidationError("You can reject request after write reason in notes:") #return False elif self.state == 3 and self.department_manager_notes != False: rec.state = 1 rec.stage = 1 rec.is_reject_required = False #raise ValidationError("You can reject request after write reason in notes:") #return False elif self.state == 4 and self.accountant_manager_notes != False: rec.state = 1 rec.stage = 1 rec.is_reject_required = False #raise ValidationError("You can reject request after write reason in notes:") #return False elif self.state == 5 and self.accountant_manager_notes != False: rec.state = 1 rec.stage = 1 rec.is_reject_required = False elif self.state == 6 and self.executive_manager_notes != False: rec.state = 1 rec.stage = 1 rec.is_reject_required = False elif self.state == 7 and self.accountant_manager_notes != False: rec.state = 1 rec.stage = 1 rec.is_reject_required = False else: if self.state == 2: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 3: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 4: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 5: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 6: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 7: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True return True @api.multi def button_to_approve(self): for rec in self: if (rec.dea_availability - rec.price_alltotal) > 0: if rec.price_alltotal > 0: if rec.assigned_to_3: rec.state = 2 rec.stage = 2 rec.is_reject_required = False elif rec.assigned_to_3 == 0: rec.state = 3 rec.stage = 2 rec.is_reject_required = False else: if rec.assigned_to: rec.state = 3 rec.stage = 2 rec.is_reject_required = False elif rec.assigned_to == 0: rec.state = 4 rec.stage = 4 rec.is_reject_required = False else: rec.state = 4 rec.stage = 4 rec.is_reject_required = False else: rec.reject_reason = 'You Must Write price Items' rec.is_reject_required = True else: raise UserError(_( "Cannot Request because budget not enough this request !")) return True @api.multi def button_to_department_manager_approved(self): for rec in self: if rec.price_alltotal < 5000: rec.state = 4 rec.stage = 3 rec.is_reject_required = False else: rec.state = 3 rec.is_reject_required = False return True @api.multi def button_to_accountant_manager_approved(self): for rec in self: if rec.price_alltotal < 50000: rec.state = 4 rec.stage = 3 rec.is_reject_required = False else: rec.state = 4 rec.stage = 3 rec.is_reject_required = False #rec.state = 'to_accountant_manager_approved' #rec.stage = 3 #rec.is_reject_required = False return True @api.multi def button_approved(self): for rec in self: if rec.price_alltotal < 50000: rec.state = 6 rec.stage = 5 rec.is_reject_required = False else: rec.state = 5 rec.stage = 5 rec.is_reject_required = False #rec.state = 'approved' #rec.stage = 5 #rec.is_reject_required = False return True @api.multi def button_ceo_approved(self): for rec in self: rec.state = 5 rec.stage = 5 rec.is_reject_required = False return True @api.multi def button_rejected(self): for rec in self: if self.state == 2 and self.direct_manager_notes != False: rec.state = 7 rec.stage = 7 rec.is_reject_required = False #raise ValidationError("You can reject request after write reason in notes:") #return False elif self.state == 3 and self.department_manager_notes != False: rec.state = 7 rec.stage = 7 rec.is_reject_required = False #raise ValidationError("You can reject request after write reason in notes:") #return False elif self.state == 4 and self.accountant_manager_notes != False: rec.state = 7 rec.stage = 7 rec.is_reject_required = False #raise ValidationError("You can reject request after write reason in notes:") #return False elif self.state == 5 and self.accountant_manager_notes != False: rec.state = 7 rec.stage = 7 rec.is_reject_required = False elif self.state == 6 and self.executive_manager_notes != False: rec.state = 7 rec.stage = 7 rec.is_reject_required = False else: if self.state == 2: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 3: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 4: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 5: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True elif self.state == 6: rec.reject_reason = 'You Could Write Reject Reason In notes' rec.is_reject_required = True return True # _constraints = [ # (button_rejected, '"You can reject request after write reason in notes :".'), #(legacy_doc1_getFilename, '"The file must be a JPG file".'), #] @api.multi def _check_the_date(self): #for rec in self: # d1 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') # new_date = d1 + datetime.timedelta(days=18) # rec.date_finish = new_date for rec in self: if rec.price_alltotal <= 500 and rec.price_alltotal > 0 and (rec.state == 4 or rec.state == 5): d1 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') new_date = d1 + datetime.timedelta(days=6) rec.date_finish = new_date elif rec.price_alltotal > 500 and rec.price_alltotal <= 5000 and (rec.state == 4 or rec.state == 5): d1 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') new_date = d1 + datetime.timedelta(days=6) rec.date_finish = new_date elif rec.price_alltotal > 5000 and rec.price_alltotal <= 100000 and (rec.state == 4 or rec.state == 5): d1 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') new_date = d1 + datetime.timedelta(days=12) rec.date_finish = new_date elif rec.price_alltotal > 100000 and (rec.state == 4 or rec.state == 5): d1 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') new_date = d1 + datetime.timedelta(days=35) rec.date_finish = new_date else: d1 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') new_date = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') rec.date_finish = new_date def change_progress(self): for rec in self: x = rec.stage if rec.stage == 15: y = 0 elif rec.stage == 1: y = 0 else: y = x * 100 / 14 rec.progress= y def change_ontime_stage(self): for rec in self: d1 = datetime.datetime.today() d2 = datetime.datetime.strptime((rec.date_start),'%Y-%m-%d') d = str((d1-d2).days + 1) rec.ontime_stage = d def change_done_stage(self): for rec in self: if rec.price_alltotal <= 500: if rec.stage == 1 and rec.ontime_stage > 1: dd = "late" rec.done1 = dd elif rec.stage ==2 and rec.ontime_stage > 2: dd = "late" rec.done1 = dd elif rec.stage ==3 and rec.ontime_stage > 4: dd = "late" rec.done1 = dd elif rec.stage ==4 and rec.ontime_stage > 4: dd = "late" rec.done1 = dd elif rec.stage ==5 and rec.ontime_stage > 4: dd = "late" rec.done1 = dd #elif rec.stage ==6 and rec.ontime_stage > 4: # dd = "late" # rec.done1 = dd elif rec.price_alltotal > 500 and rec.price_alltotal <= 5000: if rec.stage == 1 and rec.ontime_stage > 1: dd = "late" rec.done1 = dd elif rec.stage ==2 and rec.ontime_stage > 2: dd = "late" rec.done1 = dd elif rec.stage ==3 and rec.ontime_stage > 5: dd = "late" rec.done1 = dd elif rec.stage ==4 and rec.ontime_stage > 5: dd = "late" rec.done1 = dd elif rec.stage ==5 and rec.ontime_stage > 5: dd = "late" rec.done1 = dd #elif rec.stage ==6 and rec.ontime_stage > 5: # dd = "late" # rec.done1 = dd elif rec.price_alltotal > 5000 and rec.price_alltotal <= 100000: if rec.stage == 1 and rec.ontime_stage > 1: dd = "late" rec.done1 = dd elif rec.stage ==2 and rec.ontime_stage > 2: dd = "late" rec.done1 = dd elif rec.stage ==3 and rec.ontime_stage > 5: dd = "late" rec.done1 = dd elif rec.stage ==4 and rec.ontime_stage > 5: dd = "late" rec.done1 = dd elif rec.stage ==5 and rec.ontime_stage > 5: dd = "late" rec.done1 = dd #elif rec.stage ==6 and rec.ontime_stage > 5: # dd = "late" # rec.done1 = dd elif rec.price_alltotal > 100000: if rec.stage == 1 and rec.ontime_stage > 1: dd = "late" rec.done1 = dd elif rec.stage ==2 and rec.ontime_stage > 2: dd = "late" rec.done1 = dd elif rec.stage ==3 and rec.ontime_stage > 7: dd = "late" rec.done1 = dd elif rec.stage ==4 and rec.ontime_stage > 7: dd = "late" rec.done1 = dd elif rec.stage ==5 and rec.ontime_stage > 7: dd = "late" rec.done1 = dd #elif rec.stage ==6 and rec.ontime_stage > 7: # dd = "late" # rec.done1 = dd elif (rec.stage ==6 or rec.stage ==7 or rec.stage ==8 or rec.stage ==9) and rec.ontime_stage > 15: dd = "late" rec.done1 = dd elif (rec.stage ==10 or rec.stage ==11 or rec.stage ==12 or rec.stage ==13) and rec.ontime_stage > 12: dd = "late" rec.done1 = dd def change_colore_on_kanban(self): for record in self: color = 0 if record.progress == 100: color = 5 else: if record.ontime < 0: color = 9 elif record.ontime == 0: if record.state == 4: color = 3 elif record.state == 3: color = 3 elif record.state == 7: color = 6 else: color = 3 elif record.ontime > 0: color = 5 elif record.progress == 100: color = 5 elif record.state == 7: color = 1 record.color = color @api.multi def write(self,vals): # Your logic goes here or call your method super(PaymentRequest, self).write(vals) for ss in self: if ss.state == 7: ss.env.cr.execute("UPDATE payment_request SET stage = 15 WHERE id = '%s' " %(ss.id)) return True
class DonationLine(models.Model): _name = 'donation.line' _description = 'Donation Lines' _rec_name = 'product_id' @api.onchange('donation_id.donation_method') def onchange_donation_method(self): res = {} res['domain'] = { 'product_id': [('donation_method', '=', donation_id.donation_method.id)] } return res @api.multi @api.depends('unit_price', 'quantity', 'donation_id.currency_id', 'donation_id.donation_date', 'donation_id.company_id') def _compute_amount_company_currency(self): for line in self: amount = line.quantity * line.unit_price line.amount = amount donation_currency = line.donation_id.currency_id.with_context( date=line.donation_id.donation_date) line.amount_company_currency = donation_currency.compute( amount, line.donation_id.company_id.currency_id) donation_id = fields.Many2one('donation.donation', string='Donation', ondelete='cascade') currency_id = fields.Many2one('res.currency', related='donation_id.currency_id', readonly=True) company_currency_id = fields.Many2one( 'res.currency', related='donation_id.company_id.currency_id', readonly=True) product_id = fields.Many2one('product.product', domain=[('donation', '=', True)], string='Product', required=True, ondelete='restrict') # quantity = fields.Integer(string='Quantity', default=1) unit_price = fields.Monetary(string='Unit Price', digits=dp.get_precision('Account'), currency_field='currency_id') amount = fields.Monetary(compute='_compute_amount_company_currency', string='Amount', currency_field='currency_id', digits=dp.get_precision('Account'), store=True) amount_company_currency = fields.Monetary( compute='_compute_amount_company_currency', string='Amount in Company Currency', currency_field='company_currency_id', digits=dp.get_precision('Account'), store=True) analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', domain=[('account_type', '!=', 'closed')], ondelete='restrict') #campaign_id = fields.Many2one( analytic_account2 = fields.Many2one( 'account.analytic.account', related='donation_id.campaign_id.analytic_account_id', string='Analytic Account', readonly=True, store=True) account_id = fields.Many2one( 'account.account', related='donation_id.donation_place.account_id', string='Account', readonly=True, store=True) #analytic2 = fields.Integer(string = 'id', compute='get_analytic_account_id_3',store=True) #analytic3 = fields.Integer(string = 'id2',related='analytic_account_id.id' ,store=True) sequence = fields.Integer('Sequence') # for the fields tax_receipt_ok and in_kind, we made an important change # between v8 and v9: in v8, it was a reglar field set by an onchange # in v9, it is a related stored field tax_receipt_ok = fields.Boolean(related='product_id.tax_receipt_ok', readonly=True, store=True) donation_method = fields.Many2one(related='product_id.donation_method', readonly=True, store=True, string='Method') in_kind = fields.Boolean(related='product_id.in_kind_donation', readonly=True, store=True, string='In Kind') #tags_id = fields.Many2many('account.analytic.tag',string = 'Tags') @api.onchange('product_id') def product_id_change(self): if self.product_id: # We should change that one day... if self.product_id.list_price: self.unit_price = self.product_id.list_price #self.env.cr.execute("INSERT INTO account_analytic_tag_donation_line_rel(donation_line_id, account_analytic_tag_id)VALUES ('%s', '%d')" %(self.id,)) #@api.one #@api.depends('analytic_account_id', 'analytic_account2') @api.model #def get_analytic_account_id_3(self): #for rec in self: #rec.env.cr.execute("SELECT method.tag_id FROM donation_line inner join donation_instrument as method on donation_line.donation_method = method.id where donation_line.id= '%s'" %(rec.id)) #res = rec.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_tag_donation_line_rel(donation_line_id, account_analytic_tag_id) VALUES ('%s','%s')" %(rec.id,res))11 #rec.analytic_tag = 1 #@api.one #@api.depends('analytic_account2') #@api.model #def get_analytic_account_id_2(self): #self.analytic2 = self.analytic_account_id.id # return self.analytic_account2.id or False #return self.analytic_account_id.id or False @api.model def get_analytic_account_id(self): #self.analytic2 = self.analytic_account_id.id return self.analytic_account2.id or False #return self.analytic_account_id.id or False @api.model def get_account_id(self): #self.analytic2 = self.analytic_account_id.id return self.account_id.id or False
class AccountPaymentGroupInvoiceWizard(models.TransientModel): _name = "account.payment.group.invoice.wizard" @api.model def default_payment_group(self): return self.env['account.payment.group'].browse( self._context.get('active_id', False)) payment_group_id = fields.Many2one( 'account.payment.group', default=default_payment_group, ondelete='cascade', required=True, ) journal_id = fields.Many2one( 'account.journal', 'Journal', required=True, ondelete='cascade', ) date_invoice = fields.Date(string='Refund Date', default=fields.Date.context_today, required=True) currency_id = fields.Many2one( related='payment_group_id.currency_id', readonly=True, ) date = fields.Date(string='Accounting Date') product_id = fields.Many2one( 'product.product', required=True, domain=[('sale_ok', '=', True)], ) tax_ids = fields.Many2many( 'account.tax', string='Taxes', ) amount_untaxed = fields.Monetary( string='Untaxed Amount', required=True, compute='_compute_amount_untaxed', inverse='_inverse_amount_untaxed', ) # we make amount total the main one and the other computed because the # normal case of use would be to know the total amount and also this amount # is the suggested one on creating the wizard amount_total = fields.Monetary(string='Total Amount', required=True) description = fields.Char(string='Reason', ) company_id = fields.Many2one( related='payment_group_id.company_id', readonly=True, ) account_analytic_id = fields.Many2one( 'account.analytic.account', 'Analytic Account', ) @api.onchange('product_id') def change_product(self): self.ensure_one() if self.payment_group_id.partner_type == 'purchase': taxes = self.product_id.supplier_taxes_id else: taxes = self.product_id.taxes_id company = self.company_id or self.env.user.company_id taxes = taxes.filtered(lambda r: r.company_id == company) self.tax_ids = self.payment_group_id.partner_id.with_context( force_company=company.id).property_account_position_id.map_tax( taxes) @api.onchange('amount_untaxed', 'tax_ids') def _inverse_amount_untaxed(self): self.ensure_one() if self.tax_ids: taxes = self.tax_ids.compute_all( self.amount_untaxed, self.company_id.currency_id, 1.0, product=self.product_id, partner=self.payment_group_id.partner_id) self.amount_total = taxes['total_included'] else: self.amount_total = self.amount_untaxed @api.depends('tax_ids', 'amount_total') def _compute_amount_untaxed(self): """ For now we implement inverse only for percent taxes. We could extend to other by simulating tax.price_include = True, computing tax and then restoring tax.price_include = False. """ self.ensure_one() if any(x.amount_type != 'percent' for x in self.tax_ids): raise ValidationError( _('You can only set amount total if taxes are of type ' 'percentage')) tax_percent = sum( [x.amount for x in self.tax_ids if not x.price_include]) total_percent = (1 + tax_percent / 100) or 1.0 self.amount_untaxed = self.amount_total / total_percent @api.onchange('payment_group_id') def change_payment_group(self): journal_type = 'sale' type_tax_use = 'sale' if self.payment_group_id.partner_type == 'purchase': journal_type = 'purchase' type_tax_use = 'purchase' journal_domain = [ ('type', '=', journal_type), ('company_id', '=', self.payment_group_id.company_id.id), ] tax_domain = [('type_tax_use', '=', type_tax_use), ('company_id', '=', self.payment_group_id.company_id.id)] self.journal_id = self.env['account.journal'].search(journal_domain, limit=1) # usually debit/credit note will be for the payment difference self.amount_total = abs(self.payment_group_id.payment_difference) return { 'domain': { 'journal_id': journal_domain, 'tax_ids': tax_domain, } } @api.multi def get_invoice_vals(self): self.ensure_one() payment_group = self.payment_group_id if payment_group.partner_type == 'supplier': invoice_type = 'in_' else: invoice_type = 'out_' if self._context.get('refund'): invoice_type += 'refund' else: invoice_type += 'invoice' return { 'name': self.description, 'date': self.date, 'date_invoice': self.date_invoice, 'origin': _('Payment id %s') % payment_group.id, 'journal_id': self.journal_id.id, 'user_id': payment_group.partner_id.user_id.id, 'partner_id': payment_group.partner_id.id, 'type': invoice_type, # 'invoice_line_ids': [('invoice_type')], } @api.multi def confirm(self): self.ensure_one() invoice = self.env['account.invoice'].create(self.get_invoice_vals()) inv_line_vals = { 'product_id': self.product_id.id, 'price_unit': self.amount_untaxed, 'invoice_id': invoice.id, 'invoice_line_tax_ids': [(6, 0, self.tax_ids.ids)], } invoice_line = self.env['account.invoice.line'].new(inv_line_vals) invoice_line._onchange_product_id() # restore chosen taxes (changed by _onchange_product_id) invoice_line.invoice_line_tax_ids = self.tax_ids line_values = invoice_line._convert_to_write(invoice_line._cache) line_values['price_unit'] = self.amount_untaxed if self.account_analytic_id: line_values['account_analytic_id'] = self.account_analytic_id.id invoice.write({'invoice_line_ids': [(0, 0, line_values)]}) invoice.compute_taxes() invoice.signal_workflow('invoice_open') # TODO this FIX is to be removed on v11. This is to fix reconcilation # of the invoice with secondary_currency and the debit note without # secondary_currency secondary_currency = self.payment_group_id.to_pay_move_line_ids.mapped( 'currency_id') if len(secondary_currency) == 1: move_lines = invoice.move_id.line_ids.filtered( lambda x: x.account_id == invoice.account_id) # use _write because move is already posted if not move_lines.mapped('currency_id'): move_lines._write({ 'currency_id': secondary_currency.id, 'amount_currency': 0.0, }) self.payment_group_id.to_pay_move_line_ids += ( invoice.open_move_line_ids)
class PurchaseOrder(models.Model): _name = "purchase.order" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Purchase Order" _order = 'date_order desc, id desc' @api.depends('order_line.price_total') def _amount_all(self): for order in self: amount_untaxed = amount_tax = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal # FORWARDPORT UP TO 10.0 if order.company_id.tax_calculation_rounding_method == 'round_globally': taxes = line.taxes_id.compute_all(line.price_unit, line.order_id.currency_id, line.product_qty, product=line.product_id, partner=line.order_id.partner_id) amount_tax += sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])) else: amount_tax += line.price_tax order.update({ 'amount_untaxed': order.currency_id.round(amount_untaxed), 'amount_tax': order.currency_id.round(amount_tax), 'amount_total': amount_untaxed + amount_tax, }) @api.depends('order_line.date_planned') def _compute_date_planned(self): for order in self: min_date = False for line in order.order_line: if not min_date or line.date_planned < min_date: min_date = line.date_planned if min_date: order.date_planned = min_date @api.depends('state', 'order_line.qty_invoiced', 'order_line.product_qty') def _get_invoiced(self): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for order in self: if order.state != 'purchase': order.invoice_status = 'no' continue if any(float_compare(line.qty_invoiced, line.product_qty, precision_digits=precision) == -1 for line in order.order_line): order.invoice_status = 'to invoice' elif all(float_compare(line.qty_invoiced, line.product_qty, precision_digits=precision) >= 0 for line in order.order_line): order.invoice_status = 'invoiced' else: order.invoice_status = 'no' @api.depends('order_line.invoice_lines.invoice_id.state') def _compute_invoice(self): for order in self: invoices = self.env['account.invoice'] for line in order.order_line: invoices |= line.invoice_lines.mapped('invoice_id') order.invoice_ids = invoices order.invoice_count = len(invoices) @api.model def _default_picking_type(self): type_obj = self.env['stock.picking.type'] company_id = self.env.context.get('company_id') or self.env.user.company_id.id types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)]) if not types: types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)]) return types[:1] @api.depends('order_line.move_ids') def _compute_picking(self): for order in self: pickings = self.env['stock.picking'] for line in order.order_line: # We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and # do some recursive search, but that could be prohibitive if not done correctly. moves = line.move_ids | line.move_ids.mapped('returned_move_ids') moves = moves.filtered(lambda r: r.state != 'cancel') pickings |= moves.mapped('picking_id') order.picking_ids = pickings order.picking_count = len(pickings) READONLY_STATES = { 'purchase': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)], } name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New') origin = fields.Char('Source Document', copy=False,\ help="Reference of the document that generated this purchase order " "request (e.g. a sale order or an internal procurement request)") partner_ref = fields.Char('Vendor Reference', copy=False,\ help="Reference of the sales order or bid sent by the vendor. " "It's used to do the matching when you receive the " "products as this reference is usually written on the " "delivery order sent by your vendor.") date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\ help="Depicts the date where the Quotation should be validated and converted into a purchase order.") date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always') dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\ help="Put an address if you want to deliver directly from the vendor to the customer. "\ "Otherwise, keep empty to deliver to your own company.") currency_id = fields.Many2one('res.currency', 'Currency', required=True, states=READONLY_STATES,\ default=lambda self: self.env.user.company_id.currency_id.id) state = fields.Selection([ ('draft', 'Draft PO'), ('sent', 'RFQ Sent'), ('to approve', 'To Approve'), ('purchase', 'Purchase Order'), ('done', 'Done'), ('cancel', 'Cancelled') ], string='Status', readonly=True, index=True, copy=False, default='draft', track_visibility='onchange') order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True) notes = fields.Text('Terms and Conditions') invoice_count = fields.Integer(compute="_compute_invoice", string='# of Invoices', copy=False, default=0) invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Invoices', copy=False) invoice_status = fields.Selection([ ('no', 'Not purchased'), ('to invoice', 'Waiting Invoices'), ('invoiced', 'Invoice Received'), ], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no') picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0) picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False) # There is no inverse function on purpose since the date may be different on each line date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', required=True, index=True, oldname='minimum_planned_date') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all') amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all') fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position') payment_term_id = fields.Many2one('account.payment.term', 'Payment Term') incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') create_uid = fields.Many2one('res.users', 'Responsible') company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id) picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ help="This will determine picking type of incoming shipment") default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\ help="Technical field used to display the Drop Ship Address", readonly=True) group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False) @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] domain = [] if name: domain = ['|', ('name', operator, name), ('partner_ref', operator, name)] pos = self.search(domain + args, limit=limit) return pos.name_get() @api.multi @api.depends('name', 'partner_ref') def name_get(self): result = [] for po in self: name = po.name if po.partner_ref: name += ' ('+po.partner_ref+')' result.append((po.id, name)) return result @api.model def create(self, vals): if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order') or '/' return super(PurchaseOrder, self).create(vals) @api.multi def unlink(self): for order in self: if not order.state == 'cancel': raise UserError(_('In order to delete a purchase order, you must cancel it first.')) return super(PurchaseOrder, self).unlink() @api.multi def copy(self, default=None): new_po = super(PurchaseOrder, self).copy(default=default) for line in new_po.order_line: seller = line.product_id._select_seller( line.product_id, partner_id=line.partner_id, quantity=line.product_qty, date=line.order_id.date_order and line.order_id.date_order[:10], uom_id=line.product_uom) line.date_planned = line._get_date_planned(seller) return new_po @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'purchase': return 'purchase.mt_rfq_approved' elif 'state' in init_values and self.state == 'to approve': return 'purchase.mt_rfq_confirmed' elif 'state' in init_values and self.state == 'done': return 'purchase.mt_rfq_done' return super(PurchaseOrder, self)._track_subtype(init_values) @api.onchange('partner_id', 'company_id') def onchange_partner_id(self): if not self.partner_id: self.fiscal_position_id = False self.payment_term_id = False self.currency_id = False else: self.fiscal_position_id = self.env['account.fiscal.position'].with_context(company_id=self.company_id.id).get_fiscal_position(self.partner_id.id) self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id return {} @api.onchange('picking_type_id') def _onchange_picking_type_id(self): if self.picking_type_id.default_location_dest_id.usage != 'customer': self.dest_address_id = False @api.multi def action_rfq_send(self): ''' This function opens a window to compose an email, with the edi purchase template message loaded by default ''' self.ensure_one() ir_model_data = self.env['ir.model.data'] try: if self.env.context.get('send_rfq', False): template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase')[1] else: template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase_done')[1] except ValueError: template_id = False try: compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1] except ValueError: compose_form_id = False ctx = dict(self.env.context or {}) ctx.update({ 'default_model': 'purchase.order', 'default_res_id': self.ids[0], 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment', }) return { 'name': _('Compose Email'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form_id, 'form')], 'view_id': compose_form_id, 'target': 'new', 'context': ctx, } @api.multi def print_quotation(self): self.write({'state': "sent"}) return self.env['report'].get_action(self, 'purchase.report_purchasequotation') @api.multi def button_approve(self): self.write({'state': 'purchase'}) self._create_picking() return {} @api.multi def button_draft(self): self.write({'state': 'draft'}) return {} @api.multi def button_confirm(self): for order in self: if order.state not in ['draft', 'sent']: continue order._add_supplier_to_product() # Deal with double validation process if order.company_id.po_double_validation == 'one_step'\ or (order.company_id.po_double_validation == 'two_step'\ and order.amount_total < self.env.user.company_id.currency_id.compute(order.company_id.po_double_validation_amount, order.currency_id))\ or order.user_has_groups('purchase.group_purchase_manager'): order.button_approve() else: order.write({'state': 'to approve'}) return {} @api.multi def button_cancel(self): for order in self: for pick in order.picking_ids: if pick.state == 'done': raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name)) for inv in order.invoice_ids: if inv and inv.state not in ('cancel', 'draft'): raise UserError(_("Unable to cancel this purchase order.i You must first cancel related vendor bills.")) for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'): pick.action_cancel() if not self.env.context.get('cancel_procurement'): procurements = order.order_line.mapped('procurement_ids') procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and r.rule_id.propagate).write({'state': 'cancel'}) procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and not r.rule_id.propagate).write({'state': 'exception'}) moves = procurements.filtered(lambda r: r.rule_id.propagate).mapped('move_dest_id') moves.filtered(lambda r: r.state != 'cancel').action_cancel() self.write({'state': 'cancel'}) @api.multi def button_done(self): self.write({'state': 'done'}) @api.multi def _get_destination_location(self): self.ensure_one() if self.dest_address_id: return self.dest_address_id.property_stock_customer.id return self.picking_type_id.default_location_dest_id.id @api.model def _prepare_picking(self): if not self.group_id: self.group_id = self.group_id.create({ 'name': self.name, 'partner_id': self.partner_id.id }) if not self.partner_id.property_stock_supplier.id: raise UserError(_("You must set a Vendor Location for this partner %s") % self.partner_id.name) return { 'picking_type_id': self.picking_type_id.id, 'partner_id': self.partner_id.id, 'date': self.date_order, 'origin': self.name, 'location_dest_id': self._get_destination_location(), 'location_id': self.partner_id.property_stock_supplier.id, 'company_id': self.company_id.id, } @api.multi def _create_picking(self): for order in self: if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]): res = order._prepare_picking() picking = self.env['stock.picking'].create(res) moves = order.order_line.filtered(lambda r: r.product_id.type in ['product', 'consu'])._create_stock_moves(picking) move_ids = moves.action_confirm() moves = self.env['stock.move'].browse(move_ids) moves.force_assign() return True @api.multi def _add_supplier_to_product(self): # Add the partner in the supplier list of the product if the supplier is not registered for # this product. We limit to 10 the number of suppliers for a product to avoid the mess that # could be caused for some generic products ("Miscellaneous"). for line in self.order_line: # Do not add a contact as a supplier partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id if partner not in line.product_id.seller_ids.mapped('name') and len(line.product_id.seller_ids) <= 10: currency = partner.property_purchase_currency_id or self.env.user.company_id.currency_id supplierinfo = { 'name': partner.id, 'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1, 'product_uom': line.product_uom.id, 'min_qty': 0.0, 'price': self.currency_id.compute(line.price_unit, currency), 'currency_id': currency.id, 'delay': 0, } vals = { 'seller_ids': [(0, 0, supplierinfo)], } try: line.product_id.write(vals) except AccessError: # no write access rights -> just ignore break @api.multi def action_view_picking(self): ''' This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately. ''' action = self.env.ref('stock.action_picking_tree') result = action.read()[0] #override the context to get rid of the default filtering on picking type result.pop('id', None) result['context'] = {} pick_ids = sum([order.picking_ids.ids for order in self], []) #choose the view_mode accordingly if len(pick_ids) > 1: result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]" elif len(pick_ids) == 1: res = self.env.ref('stock.view_picking_form', False) result['views'] = [(res and res.id or False, 'form')] result['res_id'] = pick_ids and pick_ids[0] or False return result @api.multi def action_view_invoice(self): ''' This function returns an action that display existing vendor bills of given purchase order ids. When only one found, show the vendor bill immediately. ''' action = self.env.ref('account.action_invoice_tree2') result = action.read()[0] #override the context to get rid of the default filtering result['context'] = {'type': 'in_invoice', 'default_purchase_id': self.id} if not self.invoice_ids: # Choose a default account journal in the same currency in case a new invoice is created journal_domain = [ ('type', '=', 'purchase'), ('company_id', '=', self.company_id.id), ('currency_id', '=', self.currency_id.id), ] default_journal_id = self.env['account.journal'].search(journal_domain, limit=1) if default_journal_id: result['context']['default_journal_id'] = default_journal_id.id else: # Use the same account journal than a previous invoice result['context']['default_journal_id'] = self.invoice_ids[0].journal_id.id #choose the view_mode accordingly if len(self.invoice_ids) != 1: result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]" elif len(self.invoice_ids) == 1: res = self.env.ref('account.invoice_supplier_form', False) result['views'] = [(res and res.id or False, 'form')] result['res_id'] = self.invoice_ids.id return result @api.multi def action_set_date_planned(self): # implementation for 9.0 where PO date_planned is not stored date_planned = self.env.context.get('date_planned') if not date_planned: return for order in self: for line in order.order_line: line.update({'date_planned': date_planned})
class AccountPaymentGroup(models.Model): _name = "account.payment.group" _description = "Payment Group" _order = "payment_date desc" _inherit = 'mail.thread' # campos copiados de payment # payment_type = fields.Selection( # [('outbound', 'Send Money'), ('inbound', 'Receive Money')], # string='Payment Type', # required=True, # readonly=True, # states={'draft': [('readonly', False)]}, # ) company_id = fields.Many2one( 'res.company', string='Company', required=True, index=True, default=lambda self: self.env.user.company_id, readonly=True, states={'draft': [('readonly', False)]}, ) partner_type = fields.Selection( [('customer', 'Customer'), ('supplier', 'Vendor')], track_visibility='always', ) partner_id = fields.Many2one( 'res.partner', string='Partner', required=True, readonly=True, states={'draft': [('readonly', False)]}, track_visibility='always', ) commercial_partner_id = fields.Many2one( related='partner_id.commercial_partner_id', readonly=True, ) currency_id = fields.Many2one( 'res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id, readonly=True, states={'draft': [('readonly', False)]}, track_visibility='always', ) payment_date = fields.Date( string='Payment Date', default=fields.Date.context_today, required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, ) communication = fields.Char( string='Memo', readonly=True, states={'draft': [('readonly', False)]}, ) notes = fields.Text( string='Notes' ) # campos nuevos # reconcile = fields.Selection([ # ('invoices', 'Invoices'), # ('move_lines', 'Entry Lines')], # required=True, # default='move_lines', # # default='invoices', # ) # rename fields or labels matched_amount = fields.Monetary( compute='_compute_matched_amounts', currency_field='currency_id', ) unmatched_amount = fields.Monetary( compute='_compute_matched_amounts', currency_field='currency_id', ) @api.multi @api.depends( 'state', 'payments_amount', 'matched_move_line_ids.payment_group_matched_amount') def _compute_matched_amounts(self): for rec in self: if rec.state != 'posted': continue # damos vuelta signo porque el payments_amount tmb lo da vuelta, # en realidad porque siempre es positivo y se define en funcion # a si es pago entrante o saliente sign = rec.partner_type == 'supplier' and -1.0 or 1.0 rec.matched_amount = sign * sum( rec.matched_move_line_ids.with_context( payment_group_id=rec.id).mapped( 'payment_group_matched_amount')) rec.unmatched_amount = rec.payments_amount - rec.matched_amount selected_finacial_debt = fields.Monetary( string='Selected Financial Debt', compute='_compute_selected_debt', ) selected_debt = fields.Monetary( # string='To Pay lines Amount', string='Selected Debt', compute='_compute_selected_debt', ) # this field is to be used by others selected_debt_untaxed = fields.Monetary( # string='To Pay lines Amount', string='Selected Debt Untaxed', compute='_compute_selected_debt', ) unreconciled_amount = fields.Monetary( string='Adjusment / Advance', readonly=True, states={'draft': [('readonly', False)]}, ) # reconciled_amount = fields.Monetary(compute='_compute_amounts') to_pay_amount = fields.Monetary( compute='_compute_to_pay_amount', inverse='_inverse_to_pay_amount', string='To Pay Amount', # string='Total To Pay Amount', readonly=True, states={'draft': [('readonly', False)]}, track_visibility='always', ) payments_amount = fields.Monetary( compute='_compute_payments_amount', string='Amount', track_visibility='always', ) # name = fields.Char( # readonly=True, # copy=False, # default="Draft Payment" # ) # The name is attributed upon post() state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('posted', 'Posted'), # ('sent', 'Sent'), # ('reconciled', 'Reconciled') ], readonly=True, default='draft', copy=False, string="Status", track_visibility='onchange', ) move_lines_domain = ( "[" "('partner_id.commercial_partner_id', '=', commercial_partner_id)," "('account_id.internal_type', '=', account_internal_type)," "('account_id.reconcile', '=', True)," "('reconciled', '=', False)," "('company_id', '=', company_id)," # '|', # ('amount_residual', '!=', False), # ('amount_residual_currency', '!=', False), "]") debt_move_line_ids = fields.Many2many( 'account.move.line', # por alguna razon el related no funciona bien ni con states ni # actualiza bien con el onchange, hacemos computado mejor compute='_compute_debt_move_line_ids', inverse='_inverse_debt_move_line_ids', string="Debt Lines", # no podemos ordenar por due date porque esta hardecodeado en # funcion _get_pair_to_reconcile help="Payment will be automatically matched with the oldest lines of " "this list (by date, no by maturity date). You can remove any line you" " dont want to be matched.", domain=move_lines_domain, readonly=True, states={'draft': [('readonly', False)]}, ) to_pay_move_line_ids = fields.Many2many( 'account.move.line', 'account_move_line_payment_group_to_pay_rel', 'payment_group_id', 'to_pay_line_id', string="To Pay Lines", help='This lines are the ones the user has selected to be paid.', copy=False, domain=move_lines_domain, # lo hacemos readonly por vista y no por aca porque el relatd si no # no funcionaba bien readonly=True, states={'draft': [('readonly', False)]}, ) matched_move_line_ids = fields.Many2many( 'account.move.line', compute='_compute_matched_move_line_ids', help='Lines that has been matched to payments, only available after ' 'payment validation', ) payment_subtype = fields.Char( compute='_compute_payment_subtype' ) pop_up = fields.Boolean( # campo que agregamos porque el invisible="context.get('pop_up')" # en las pages no se comportaba bien con los attrs compute='_compute_payment_pop_up', default=lambda x: x._context.get('pop_up', False), ) @api.multi @api.depends('to_pay_move_line_ids') def _compute_debt_move_line_ids(self): for rec in self: rec.debt_move_line_ids = rec.to_pay_move_line_ids @api.multi @api.onchange('debt_move_line_ids') def _inverse_debt_move_line_ids(self): for rec in self: rec.to_pay_move_line_ids = rec.debt_move_line_ids @api.multi def _compute_payment_pop_up(self): pop_up = self._context.get('pop_up', False) for rec in self: rec.pop_up = pop_up @api.multi @api.depends('company_id.double_validation', 'partner_type') def _compute_payment_subtype(self): for rec in self: if (rec.partner_type == 'supplier' and rec.company_id.double_validation): payment_subtype = 'double_validation' else: payment_subtype = 'simple' rec.payment_subtype = payment_subtype @api.one def _compute_matched_move_line_ids(self): """ Lar partial reconcile vinculan dos apuntes con credit_move_id y debit_move_id. Buscamos primeros todas las que tienen en credit_move_id algun apunte de los que se genero con un pago, etnonces la contrapartida (debit_move_id), son cosas que se pagaron con este pago. Repetimos al revz (debit_move_id vs credit_move_id) """ lines = self.move_line_ids.browse() # not sure why but self.move_line_ids dont work the same way payment_lines = self.payment_ids.mapped('move_line_ids') # este metodo deberia ser mas eficiente que el de abajo reconciles = self.env['account.partial.reconcile'].search([ ('credit_move_id', 'in', payment_lines.ids)]) lines |= reconciles.mapped('debit_move_id') reconciles = self.env['account.partial.reconcile'].search([ ('debit_move_id', 'in', payment_lines.ids)]) lines |= reconciles.mapped('credit_move_id') # otro metodo, tal vez mas claro pero menos eficiente # for aml in payment_lines: # if aml.account_id.reconcile: # if aml.credit > 0: # lines |= aml.matched_debit_ids.mapped('debit_move_id') # else: # lines |= aml.matched_credit_ids.mapped('credit_move_id') self.matched_move_line_ids = lines - payment_lines payment_difference = fields.Monetary( compute='_compute_payment_difference', # TODO rename field or remove string # string='Remaining Residual', readonly=True, string="Payments Difference", help="Difference between selected debt (or to pay amount) and " "payments amount" ) # TODO por ahora no implementamos # payment_difference_handling = fields.Selection( # [('open', 'Keep open'), ('reconcile', 'Mark invoice as fully paid')], # default='open', # string="Payment Difference", # copy=False # ) # TODO add journal? # writeoff_account_id = fields.Many2one( # 'account.account', # string="Difference Account", # domain=[('deprecated', '=', False)], # copy=False # ) payment_ids = fields.One2many( 'account.payment', 'payment_group_id', string='Payment Lines', ondelete='cascade', copy=False, readonly=True, states={ 'draft': [('readonly', False)], 'confirmed': [('readonly', False)]}, ) move_line_ids = fields.One2many( related='payment_ids.move_line_ids', readonly=True, copy=False, ) account_internal_type = fields.Char( compute='_compute_account_internal_type' ) @api.multi @api.depends('partner_type') def _compute_account_internal_type(self): for rec in self: if rec.partner_type: rec.account_internal_type = MAP_PARTNER_TYPE_ACCOUNT_TYPE[ rec.partner_type] @api.multi @api.depends('to_pay_amount', 'payments_amount') def _compute_payment_difference(self): for rec in self: # if rec.payment_subtype != 'double_validation': # continue rec.payment_difference = rec.to_pay_amount - rec.payments_amount @api.multi @api.depends('payment_ids.amount_company_currency') def _compute_payments_amount(self): for rec in self: rec.payments_amount = sum(rec.payment_ids.mapped( 'amount_company_currency')) # payments_amount = sum([ # x.payment_type == 'inbound' and # x.amount_company_currency or -x.amount_company_currency for # x in rec.payment_ids]) # rec.payments_amount = ( # rec.partner_type == 'supplier' and # -payments_amount or payments_amount) # TODO analizar en v10 # el onchange no funciona bien en o2m, si usamos write se escribe pero no # se actualiza en interfaz lo cual puede ser confuzo, por ahora lo # comentamos # @api.onchange('payment_date') # def change_payment_date(self): # # self.payment_ids.write({'payment_date': self.payment_date}) # for line in self.payment_ids: # line.payment_date = self.payment_date @api.one # @api.onchange( @api.depends( # 'to_pay_move_line_ids', 'to_pay_move_line_ids.amount_residual', 'to_pay_move_line_ids.amount_residual_currency', 'to_pay_move_line_ids.currency_id', 'to_pay_move_line_ids.invoice_id', 'payment_date', 'currency_id', ) # @api.constrains( # 'to_pay_move_line_ids', # 'payment_date', # 'currency_id', # ) # def set_selected_debt(self): def _compute_selected_debt(self): # # we dont make it computed because we want to store value. # # TODO check if odoo implement this kind of hybrid field # payment_currency = self.currency_id or self.company_id.currency_id # total_untaxed = total = 0 # for rml in self.to_pay_move_line_ids: # # factor for total_untaxed # invoice = rml.invoice_id # factor = invoice and invoice._get_tax_factor() or 1.0 # # si tiene moneda y es distinta convertimos el monto de moneda # # si tiene moneda y es igual llevamos el monto de moneda # # si no tiene moneda y es distinta convertimos el monto comun # # si no tiene moneda y es igual llevamos el monto comun # if rml.currency_id: # if rml.currency_id != payment_currency: # line_amount = rml.currency_id.with_context( # date=self.payment_date).compute( # rml.amount_residual_currency, payment_currency) # else: # line_amount = rml.amount_residual_currency # else: # if self.company_id.currency_id != payment_currency: # line_amount = self.company_id.currency_id.with_context( # date=self.payment_date).compute( # rml.amount_residual, payment_currency) # else: # line_amount = rml.amount_residual # total += line_amount # total_untaxed += line_amount * factor # self.selected_debt = abs(total) # self.selected_debt_untaxed = abs(total_untaxed) selected_finacial_debt = 0.0 selected_debt = 0.0 selected_debt_untaxed = 0.0 for line in self.to_pay_move_line_ids: selected_finacial_debt += line.financial_amount_residual selected_debt += line.amount_residual # factor for total_untaxed invoice = line.invoice_id factor = invoice and invoice._get_tax_factor() or 1.0 selected_debt_untaxed += line.amount_residual * factor sign = self.partner_type == 'supplier' and -1.0 or 1.0 self.selected_finacial_debt = selected_finacial_debt * sign self.selected_debt = selected_debt * sign self.selected_debt_untaxed = selected_debt_untaxed * sign @api.multi @api.depends( 'selected_debt', 'unreconciled_amount') def _compute_to_pay_amount(self): for rec in self: rec.to_pay_amount = rec.selected_debt + rec.unreconciled_amount @api.multi def _inverse_to_pay_amount(self): for rec in self: rec.unreconciled_amount = rec.to_pay_amount - rec.selected_debt @api.onchange('partner_id', 'partner_type', 'company_id') def _refresh_payments_and_move_lines(self): # clean actual invoice and payments # no hace falta if self._context.get('pop_up'): return # not sure why but state field is false on payments so they can # not be unliked, this fix that self.invalidate_cache(['payment_ids']) self.payment_ids.unlink() self.add_all() # if self.payment_subtype == 'double_validation': # self._add_all('to_pay_move_line_ids') # else: # self._add_all('debt_move_line_ids') # if self.to_pay_move_line_ids: # raise UserError('asdasdasd') # else: # self.debt_move_line_ids = False # self.payment_ids.unlink() # self.add_all() @api.multi def add_all(self): for rec in self: # TODO ver si es necesario agregar un remove o el update las borra domain = [ ('partner_id.commercial_partner_id', '=', rec.commercial_partner_id.id), ('account_id.internal_type', '=', rec.account_internal_type), ('account_id.reconcile', '=', True), ('reconciled', '=', False), ('company_id', '=', rec.company_id.id), # '|', # ('amount_residual', '!=', False), # ('amount_residual_currency', '!=', False), ] rec.to_pay_move_line_ids = rec.env['account.move.line'].search( domain) @api.multi def remove_all(self): self.to_pay_move_line_ids = False @api.model def default_get(self, fields): # TODO si usamos los move lines esto no haria falta rec = super(AccountPaymentGroup, self).default_get(fields) to_pay_move_line_ids = self._context.get('to_pay_move_line_ids') to_pay_move_lines = self.env['account.move.line'].browse( to_pay_move_line_ids).filtered(lambda x: ( x.account_id.reconcile and x.account_id.internal_type in ('receivable', 'payable'))) if to_pay_move_lines: partner = to_pay_move_lines.mapped('partner_id') if len(partner) != 1: raise ValidationError(_( 'You can not send to pay lines from different partners')) internal_type = to_pay_move_lines.mapped( 'account_id.internal_type') if len(internal_type) != 1: raise ValidationError(_( 'You can not send to pay lines from different partners')) rec['partner_id'] = partner[0].id rec['partner_type'] = MAP_ACCOUNT_TYPE_PARTNER_TYPE[ internal_type[0]] # rec['currency_id'] = invoice['currency_id'][0] # rec['payment_type'] = ( # internal_type[0] == 'receivable' and # 'inbound' or 'outbound') rec['to_pay_move_line_ids'] = [(6, False, to_pay_move_line_ids)] return rec # print 'rec', rec # invoice_defaults = self.resolve_2many_commands( # 'invoice_ids', rec.get('invoice_ids')) # print 'aaaaaa' # print 'aaaaaa', self._context # print 'aaaaaa', invoice_defaults # print 'aaaaaa', invoice_defaults # if invoice_defaults and len(invoice_defaults) == 1: # invoice = invoice_defaults[0] # rec['communication'] = invoice[ # 'reference'] or invoice['name'] or invoice['number'] # rec['currency_id'] = invoice['currency_id'][0] # rec['payment_type'] = invoice['type'] in ( # 'out_invoice', 'in_refund') and 'inbound' or 'outbound' # rec['partner_type'] = MAP_INVOICE_TYPE_PARTNER_TYPE[ # invoice['type']] # rec['partner_id'] = invoice['partner_id'][0] # # rec['amount'] = invoice['residual'] # print 'rec', rec @api.multi def button_journal_entries(self): return { 'name': _('Journal Items'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move.line', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('payment_id', 'in', self.payment_ids.ids)], } @api.multi def unreconcile(self): for rec in self: rec.payment_ids.unreconcile() # TODO en alguos casos setear sent como en payment? rec.write({'state': 'posted'}) @api.multi def cancel(self): for rec in self: # because child payments dont have invoices we remove reconcile for move in rec.move_line_ids.mapped('move_id'): rec.matched_move_line_ids.remove_move_reconcile() # TODO borrar esto si con el de arriba va bien # if rec.to_pay_move_line_ids: # move.line_ids.remove_move_reconcile() rec.payment_ids.cancel() rec.state = 'draft' @api.multi def unlink(self): if any(rec.state != 'draft' for rec in self): raise ValidationError(_( "You can not delete a payment that is already posted")) return super(AccountPaymentGroup, self).unlink() @api.multi def confirm(self): for rec in self: accounts = rec.to_pay_move_line_ids.mapped('account_id') if len(accounts) > 1: raise ValidationError(_( 'To Pay Lines must be of the same account!')) rec.state = 'confirmed' @api.multi def post(self): # dont know yet why, but if we came from an invoice context values # break behaviour, for eg. with demo user error writing account.account # and with other users, error with block date of accounting # TODO we should look for a better way to solve this self = self.with_context({}) for rec in self: # TODO if we want to allow writeoff then we can disable this # constrain and send writeoff_journal_id and writeoff_acc_id if not rec.payment_ids: raise ValidationError(_( 'You can not confirm a payment group without payment ' 'lines!')) if (rec.payment_subtype == 'double_validation' and rec.payment_difference): raise ValidationError(_( 'To Pay Amount and Payment Amount must be equal!')) writeoff_acc_id = False writeoff_journal_id = False rec.payment_ids.post() counterpart_aml = rec.payment_ids.mapped('move_line_ids').filtered( lambda r: not r.reconciled and r.account_id.internal_type in ( 'payable', 'receivable')) (counterpart_aml + (rec.to_pay_move_line_ids)).reconcile( writeoff_acc_id, writeoff_journal_id) rec.state = 'posted'
class SaleOrderGrantExpond(models.Model): _inherit = 'sale.order' amount_grant_number = fields.Integer(string='总期数', readonly=True) amount_grant_already_number = fields.Integer(string='已出期数', readonly=True) amount_grant_surplus_next = fields.Monetary(string='下期发放', readonly=True) amount_grant_surplus_all = fields.Monetary(string='剩余发放', readonly=True) amount_grant_rate = fields.Float(string=u'税点', requird=True, digits=(16, 3), readonly=True) amount_grant_confirm = fields.Char( string='下期发放情况', help='这是本期补助发放情况,双方全部确认后,即认为本次客户这正常获得补助', readonly=True) amount_grant_confirm_a = fields.Selection([('yes', '已确认'), ('no', '未确认')], readonly=True) amount_grant_confirm_b = fields.Selection([('yes', '已确认'), ('no', '未确认')], readonly=True) amount_grant_state = fields.Selection([('draft', '待确认'), ('in', '发放中'), ('done', '已完毕'), ('other', '退费')], string='补贴进度', readonly=True) amount_grant_state_backup = fields.Selection([('draft', '待确认'), ('in', '发放中'), ('done', '已完毕'), ('other', '退费')], string='补贴进度', readonly=True, help='用作在退款时备份补贴进度字段,可做撤销用') # 清空补贴金额以后,清空补贴相关字段 @api.constrains('amount_grant', 'amount_grant_number') @api.onchange('amount_grant', 'amount_grant_number') def check_amount_grant(self): self.update({ 'amount_grant_already_number': 0, 'amount_grant_surplus_next': 0.0, 'amount_grant_confirm': False, 'amount_grant_state': False, }) self.partner_id.write({'sale_order_grant_message': False}) if self.amount_grant == 0.0: self.update({ 'amount_grant_rate': 5.0, 'amount_grant_surplus_all': 0.0, 'amount_grant_confirm_a': False, 'amount_grant_confirm_b': False, }) elif self.amount_grant > 0.0: rate = self.amount_grant_rate if ( self.amount_grant_rate != 0.0) else 5.0 self.update({ 'amount_grant_rate': 5.0, 'amount_grant_surplus_all': self.amount_grant - self.amount_grant * (rate / 100.0), 'amount_grant_confirm_a': 'no', 'amount_grant_confirm_b': 'no', }) @api.onchange('amount_grant_number') def _check_amount_grant_number(self): if self.amount_grant_number > 0.0: self.check_amount_grant() self.amount_grant_surplus_all = self.amount_grant - self.amount_grant * ( self.amount_grant_rate / 100.0) # 重载'确认销售'按钮,使点击时客户的补助状态随补助数值显示为'无'或者'带确认' @api.one def action_confirm(self): self.write({ 'amount_grant_confirm_a': 'no', 'amount_grant_confirm_b': 'no' }) if self.amount_grant == 0.0: self.partner_id.write({'sale_grant_state': 'none'}) elif self.amount_grant > 0.0: self.amount_grant_surplus_all = self.amount_grant - self.amount_grant * ( self.amount_grant_rate / 100.0) self.amount_grant_state = 'draft' self.partner_id.write({'sale_grant_state': 'draft'}) return super(SaleOrderGrantExpond, self).action_confirm() # 重载'取消'按钮,使点击时客户的补助状态变为none,因为无论何种情况下,报价单状态这个字段始终为'无' # 取消当前订单时,还需要判断该用户是否有其他补贴在流程中,若存在,则不重置客户状态为'无' @api.multi def action_cancel(self): self.update({ 'amount_grant_state': False, }) orders = self.env['sale.order'].search([ ('id', '!=', self.id), ('partner_id', '=', self.partner_id.id), ('amount_grant_state', 'in', ('draft', 'in', 'done')) ]) if len(orders) == 0: self.partner_id.write({ 'sale_grant_state': 'none', 'grant_change_sale_number_wizard': False, }) self.check_amount_grant() return super(SaleOrderGrantExpond, self).action_cancel() # 当退费发起,先查询该客户是否有正常的订单,有则仅将本订单补贴状态异常,无则同时将客户补贴状态异常 @api.multi def state_apply(self): self.amount_grant_state_backup = self.amount_grant_state self.write({'amount_grant_state': 'other'}) orders = self.env['sale.order'].search([ ('id', '!=', self.id), ('partner_id', '=', self.partner_id.id), ('amount_grant_state', 'in', ('draft', 'in', 'done')) ]) if len(orders) == 0: self.partner_id.write({'sale_grant_state': 'other'}) return super(SaleOrderGrantExpond, self).state_apply() # 当退费发起,先查询该客户是否有正常的订单,有则仅将本订单补贴状态异常,无则同时将客户补贴状态异常 @api.multi def state_sale_to(self): self.amount_grant_state = self.amount_grant_state_backup orders = self.env['sale.order'].search([ ('id', '!=', self.id), ('partner_id', '=', self.partner_id.id), ('amount_grant_state', 'in', ('draft', 'in', 'done')) ]) if len(orders) == 0: self.partner_id.write( {'sale_grant_state': self.amount_grant_state}) return super(SaleOrderGrantExpond, self).state_sale_to() # 财务与班主任每一期点击确认,双方确认后,一期通过,直到最后一期完成 @api.multi def _change_number_state(self): if self.amount_grant_confirm_a and (self.amount_grant_already_number < self.amount_grant_number): self.amount_grant_already_number += 1 routine = self.amount_grant / self.amount_grant_number last = routine - (self.amount_grant * (self.amount_grant_rate / 100.0)) timestamp = datetime.datetime.strftime( datetime.datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT) # 确认全部已完成,订单'补贴进度'、客户'补贴进度'为完成,同时清空下次补贴和剩余补贴 if self.amount_grant_already_number == self.amount_grant_number: self.update({ 'amount_grant_state': 'done', 'amount_grant_surplus_next': 0.0, 'amount_grant_surplus_all': 0.0, 'amount_grant_confirm': '全' + str(self.amount_grant_already_number) + '/' + str(self.amount_grant_number) + '期▶全部完成于' + timestamp, }) self.partner_id.write({ 'sale_grant_state': 'done', 'sale_order_grant_message': self.name + ':' + self.amount_grant_confirm, }) # 未完成最后一期,每期就会计算字段 if self.amount_grant_already_number < self.amount_grant_number: if self.amount_grant_number == ( self.amount_grant_already_number + 1): self.amount_grant_surplus_next = last self.amount_grant_surplus_all = last else: self.amount_grant_surplus_next = routine self.amount_grant_surplus_all = self.amount_grant_surplus_all - self.amount_grant_surplus_next self.update({ 'amount_grant_state': 'in', 'amount_grant_confirm_a': 'no', 'amount_grant_confirm_b': 'no', 'amount_grant_confirm': '第' + str(self.amount_grant_already_number + 1) + '/' + str(self.amount_grant_number) + '期▶' + str(self.amount_grant_surplus_next) + '¥▶上期' + timestamp + '完成', }) self.partner_id.sale_order_grant_message = self.name + ':' + self.amount_grant_confirm # 财务按钮动作 @api.multi def change_already_number_by_a(self): self.amount_grant_confirm_a = 'yes' if self.amount_grant_confirm_a == self.amount_grant_confirm_b == 'yes': self._change_number_state() else: timestamp = datetime.datetime.strftime( datetime.datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT) self.amount_grant_confirm = '第' + str( self.amount_grant_already_number + 1) + '/' + str(self.amount_grant_number) + '期▶' + str( self.amount_grant_surplus_next) + '¥▶财务' + timestamp + '确认' self.partner_id.write({ 'sale_order_grant_message': self.name + ':' + self.amount_grant_confirm }) #班主任按钮动作 @api.multi def change_already_number_by_b(self): self.amount_grant_confirm_b = 'yes' if self.amount_grant_confirm_a == self.amount_grant_confirm_b == 'yes': self._change_number_state() else: timestamp = datetime.datetime.strftime( datetime.datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT) self.amount_grant_confirm = '第' + str( self.amount_grant_already_number + 1 ) + '/' + str(self.amount_grant_number) + '期▶' + str( self.amount_grant_surplus_next) + '¥▶班主任' + timestamp + '确认' self.partner_id.write({ 'sale_order_grant_message': self.name + ':' + self.amount_grant_confirm }) #生成草稿付款单 user = self.env.user.employee_ids pay_type = self.env['expense.type'].search([('name', '=', '学员补贴款')]) ticket_type = self.env['invoice.type'].search([('name', '=', '内部票据')]) did = False dmid = False try: for employee in self.user_id.employee_ids: department = employee.department_id did = department.id except: did = False try: for employee in self.user_id.employee_ids: department = employee.department_id dmid = department.manager_id.id except: dmid = False vals = { 'leixing_sel': 'c', 'leixing': self.env['email.type'].search([('name', '=', '学员生活补助费')]).id, 'name': self.env.user.id, 'department': did, 'center': self.env.user.company_id.id, 'chinese_name': user.name, 'pay_danwei': self.partner_id.name, 'pay_mode': 'zhuangzhang', 'department_manager': dmid, 'pay_date': datetime.date.today(), 'kaihu': self.bankaddress, 'zhanghu': self.banknumber, 'pay_ids': [((0, 0, { 'pay_type': pay_type.id, 'pay_money': self.amount_grant_surplus_next, 'whether_ticket': 'Yes', 'ticket_type': ticket_type.id, 'pay_reason': '学员生活补贴', }))] } pay_account = self.env['pay.account'].create(vals)
class SaleOrderLine(models.Model): _name = 'sale.order.line' _description = 'Sales Order Line' _order = 'order_id, layout_category_id, sequence, id' @api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced') def _compute_invoice_status(self): """ Compute the invoice status of a SO line. Possible statuses: - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to invoice. This is also hte default value if the conditions of no other status is met. - to invoice: we refer to the quantity to invoice of the line. Refer to method `_get_to_invoice_qty()` for more information on how this quantity is calculated. - upselling: this is possible only for a product invoiced on ordered quantities for which we delivered more than expected. The could arise if, for example, a project took more time than expected but we decided not to invoice the extra cost to the client. This occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity is removed from the list. - invoiced: the quantity invoiced is larger or equal to the quantity ordered. """ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for line in self: if line.state not in ('sale', 'done'): line.invoice_status = 'no' elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = 'to invoice' elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1: line.invoice_status = 'upselling' elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0: line.invoice_status = 'invoiced' else: line.invoice_status = 'no' @api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id') def _compute_amount(self): """ Compute the amounts of the SO line. """ for line in self: price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id) line.update({ 'price_tax': taxes['total_included'] - taxes['total_excluded'], 'price_total': taxes['total_included'], 'price_subtotal': taxes['total_excluded'], }) @api.depends('product_id.invoice_policy', 'order_id.state') def _compute_qty_delivered_updateable(self): for line in self: line.qty_delivered_updateable = line.product_id.invoice_policy in ('order', 'delivery') and line.order_id.state == 'sale' and line.product_id.track_service == 'manual' @api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state') def _get_to_invoice_qty(self): """ Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is calculated from the ordered quantity. Otherwise, the quantity delivered is used. """ for line in self: if line.order_id.state in ['sale', 'done']: if line.product_id.invoice_policy == 'order': line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced else: line.qty_to_invoice = line.qty_delivered - line.qty_invoiced else: line.qty_to_invoice = 0 @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity') def _get_invoice_qty(self): """ Compute the quantity invoiced. If case of a refund, the quantity invoiced is decreased. Note that this is the case only if the refund is generated from the SO and that is intentional: if a refund made would automatically decrease the invoiced quantity, then there is a risk of reinvoicing it automatically, which may not be wanted at all. That's why the refund has to be created from the SO """ for line in self: qty_invoiced = 0.0 for invoice_line in line.invoice_lines: if invoice_line.invoice_id.state != 'cancel': if invoice_line.invoice_id.type == 'out_invoice': qty_invoiced += invoice_line.quantity elif invoice_line.invoice_id.type == 'out_refund': qty_invoiced -= invoice_line.quantity line.qty_invoiced = qty_invoiced @api.depends('price_subtotal', 'product_uom_qty') def _get_price_reduce(self): for line in self: line.price_reduce = line.price_subtotal / line.product_uom_qty if line.product_uom_qty else 0.0 @api.multi def _compute_tax_id(self): for line in self: fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id if fpos: # The superuser is used by website_sale in order to create a sale order. We need to make # sure we only select the taxes related to the company of the partner. This should only # apply if the partner is linked to a company. if self.env.uid == SUPERUSER_ID and line.order_id.company_id: taxes = fpos.map_tax(line.product_id.taxes_id).filtered(lambda r: r.company_id == line.order_id.company_id) else: taxes = fpos.map_tax(line.product_id.taxes_id) line.tax_id = taxes else: line.tax_id = line.product_id.taxes_id if line.product_id.taxes_id else False @api.multi def _prepare_order_line_procurement(self, group_id=False): self.ensure_one() return { 'name': self.name, 'origin': self.order_id.name, 'date_planned': datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=self.customer_lead), 'product_id': self.product_id.id, 'product_qty': self.product_uom_qty, 'product_uom': self.product_uom.id, 'company_id': self.order_id.company_id.id, 'group_id': group_id, 'sale_line_id': self.id } @api.multi def _action_procurement_create(self): """ Create procurements based on quantity ordered. If the quantity is increased, new procurements are created. If the quantity is decreased, no automated action is taken. """ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') new_procs = self.env['procurement.order'] #Empty recordset for line in self: if line.state != 'sale' or not line.product_id._need_procurement(): continue qty = 0.0 for proc in line.procurement_ids: qty += proc.product_qty if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0: continue if not line.order_id.procurement_group_id: vals = line.order_id._prepare_procurement_group() line.order_id.procurement_group_id = self.env["procurement.group"].create(vals) vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id) vals['product_qty'] = line.product_uom_qty - qty new_proc = self.env["procurement.order"].create(vals) new_procs += new_proc new_procs.run() return new_procs @api.model def _get_analytic_invoice_policy(self): return ['cost'] @api.model def _get_analytic_track_service(self): return [] @api.model def create(self, values): onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_id'] if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields): line = self.new(values) line.product_id_change() for field in onchange_fields: if field not in values: values[field] = line._fields[field].convert_to_write(line[field], line) line = super(SaleOrderLine, self).create(values) if line.state == 'sale': if (not line.order_id.project_id and (line.product_id.track_service in self._get_analytic_track_service() or line.product_id.invoice_policy in self._get_analytic_invoice_policy())): line.order_id._create_analytic_account() line._action_procurement_create() return line @api.multi def write(self, values): lines = False if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') lines = self.filtered( lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) result = super(SaleOrderLine, self).write(values) if lines: lines._action_procurement_create() return result order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False) name = fields.Text(string='Description', required=True) sequence = fields.Integer(string='Sequence', default=10) invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False) invoice_status = fields.Selection([ ('upselling', 'Upselling Opportunity'), ('invoiced', 'Fully Invoiced'), ('to invoice', 'To Invoice'), ('no', 'Nothing to Invoice') ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no') price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'), default=0.0) price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True) price_tax = fields.Monetary(compute='_compute_amount', string='Taxes', readonly=True, store=True) price_total = fields.Monetary(compute='_compute_amount', string='Total', readonly=True, store=True) price_reduce = fields.Monetary(compute='_get_price_reduce', string='Price Reduce', readonly=True, store=True) tax_id = fields.Many2many('account.tax', string='Taxes') discount = fields.Float(string='Discount (%)', digits=dp.get_precision('Discount'), default=0.0) product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict', required=True) product_uom_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0) product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True) qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True) qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0) qty_to_invoice = fields.Float( compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True, digits=dp.get_precision('Product Unit of Measure'), default=0.0) qty_invoiced = fields.Float( compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True, digits=dp.get_precision('Product Unit of Measure'), default=0.0) salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', readonly=True) currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, readonly=True) order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer') state = fields.Selection([ ('draft', 'Quotation'), ('sent', 'Quotation Sent'), ('sale', 'Sale Order'), ('done', 'Done'), ('cancel', 'Cancelled'), ], related='order_id.state', string='Order Status', readonly=True, copy=False, store=True, default='draft') customer_lead = fields.Float( 'Delivery Lead Time', required=True, default=0.0, help="Number of days between the order confirmation and the shipping of the products to the customer", oldname="delay") procurement_ids = fields.One2many('procurement.order', 'sale_line_id', string='Procurements') layout_category_id = fields.Many2one('sale.layout_category', string='Section') layout_category_sequence = fields.Integer(related='layout_category_id.sequence', string='Layout Sequence', store=True) # Store is intentionally set in order to keep the "historic" order. @api.multi def _prepare_invoice_line(self, qty): """ Prepare the dict of values to create the new invoice line for a sales order line. :param qty: float quantity to invoice """ self.ensure_one() res = {} account = self.product_id.property_account_income_id or self.product_id.categ_id.property_account_income_categ_id if not account: raise UserError(_('Please define income account for this product: "%s" (id:%d) - or for its category: "%s".') % \ (self.product_id.name, self.product_id.id, self.product_id.categ_id.name)) fpos = self.order_id.fiscal_position_id or self.order_id.partner_id.property_account_position_id if fpos: account = fpos.map_account(account) res = { 'name': self.name, 'sequence': self.sequence, 'origin': self.order_id.name, 'account_id': account.id, 'price_unit': self.price_unit, 'quantity': qty, 'discount': self.discount, 'uom_id': self.product_uom.id, 'product_id': self.product_id.id or False, 'layout_category_id': self.layout_category_id and self.layout_category_id.id or False, 'product_id': self.product_id.id or False, 'invoice_line_tax_ids': [(6, 0, self.tax_id.ids)], 'account_analytic_id': self.order_id.project_id.id, } return res @api.multi def invoice_line_create(self, invoice_id, qty): """ Create an invoice line. The quantity to invoice can be positive (invoice) or negative (refund). :param invoice_id: integer :param qty: float quantity to invoice """ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for line in self: if not float_is_zero(qty, precision_digits=precision): vals = line._prepare_invoice_line(qty=qty) vals.update({'invoice_id': invoice_id, 'sale_line_ids': [(6, 0, [line.id])]}) self.env['account.invoice.line'].create(vals) @api.multi @api.onchange('product_id') def product_id_change(self): if not self.product_id: return {'domain': {'product_uom': []}} vals = {} domain = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]} if not self.product_uom or (self.product_id.uom_id.category_id.id != self.product_uom.category_id.id): vals['product_uom'] = self.product_id.uom_id product = self.product_id.with_context( lang=self.order_id.partner_id.lang, partner=self.order_id.partner_id.id, quantity=self.product_uom_qty, date=self.order_id.date_order, pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id ) name = product.name_get()[0][1] if product.description_sale: name += '\n' + product.description_sale vals['name'] = name self._compute_tax_id() if self.order_id.pricelist_id and self.order_id.partner_id: vals['price_unit'] = self.env['account.tax']._fix_tax_included_price(product.price, product.taxes_id, self.tax_id) self.update(vals) title = False message = False warning = {} if product.sale_line_warn != 'no-message': title = _("Warning for %s") % product.name message = product.sale_line_warn_msg warning['title'] = title warning['message'] = message if product.sale_line_warn == 'block': self.product_id = False return {'warning': warning} return {'domain': domain} @api.onchange('product_uom', 'product_uom_qty') def product_uom_change(self): if not self.product_uom: self.price_unit = 0.0 return if self.order_id.pricelist_id and self.order_id.partner_id: product = self.product_id.with_context( lang=self.order_id.partner_id.lang, partner=self.order_id.partner_id.id, quantity=self.product_uom_qty, date_order=self.order_id.date_order, pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id, fiscal_position=self.env.context.get('fiscal_position') ) self.price_unit = self.env['account.tax']._fix_tax_included_price(product.price, product.taxes_id, self.tax_id) @api.multi def unlink(self): if self.filtered(lambda x: x.state in ('sale', 'done')): raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.')) return super(SaleOrderLine, self).unlink() @api.multi def _get_delivered_qty(self): ''' Intended to be overridden in sale_stock and sale_mrp :return: the quantity delivered :rtype: float ''' return 0.0
class SaleOrder(models.Model): _name = "sale.order" _inherit = ['mail.thread', 'ir.needaction_mixin'] _description = "Sales Order" _order = 'date_order desc, id desc' @api.depends('order_line.price_total') def _amount_all(self): """ Compute the total amounts of the SO. """ for order in self: amount_untaxed = amount_tax = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal amount_tax += line.price_tax order.update({ 'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed), 'amount_tax': order.pricelist_id.currency_id.round(amount_tax), 'amount_total': amount_untaxed + amount_tax, }) @api.depends('state', 'order_line.invoice_status') def _get_invoiced(self): """ Compute the invoice status of a SO. Possible statuses: - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to invoice. This is also hte default value if the conditions of no other status is met. - to invoice: if any SO line is 'to invoice', the whole SO is 'to invoice' - invoiced: if all SO lines are invoiced, the SO is invoiced. - upselling: if all SO lines are invoiced or upselling, the status is upselling. The invoice_ids are obtained thanks to the invoice lines of the SO lines, and we also search for possible refunds created directly from existing invoices. This is necessary since such a refund is not directly linked to the SO. """ for order in self: invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id') # Search for refunds as well refund_ids = self.env['account.invoice'].browse() if invoice_ids: refund_ids = refund_ids.search([('type', '=', 'out_refund'), ('origin', 'in', invoice_ids.mapped('number')), ('origin', '!=', False)]) line_invoice_status = [line.invoice_status for line in order.order_line] if order.state not in ('sale', 'done'): invoice_status = 'no' elif any(invoice_status == 'to invoice' for invoice_status in line_invoice_status): invoice_status = 'to invoice' elif all(invoice_status == 'invoiced' for invoice_status in line_invoice_status): invoice_status = 'invoiced' elif all(invoice_status in ['invoiced', 'upselling'] for invoice_status in line_invoice_status): invoice_status = 'upselling' else: invoice_status = 'no' order.update({ 'invoice_count': len(set(invoice_ids.ids + refund_ids.ids)), 'invoice_ids': invoice_ids.ids + refund_ids.ids, 'invoice_status': invoice_status }) @api.model def _default_note(self): return self.env.user.company_id.sale_note @api.model def _get_default_team(self): return self.env['crm.team']._get_default_team_id() @api.onchange('fiscal_position_id') def _compute_tax_id(self): """ Trigger the recompute of the taxes if the fiscal position is changed on the SO. """ for order in self: order.order_line._compute_tax_id() name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, index=True, default=lambda self: _('New')) origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.") client_order_ref = fields.Char(string='Customer Reference', copy=False) state = fields.Selection([ ('draft', 'Quotation'), ('sent', 'Quotation Sent'), ('sale', 'Sale Order'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', default='draft') date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now) validity_date = fields.Date(string='Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Manually set the expiration date of your quotation (offer), or it will set the date automatically based on the template if online quotation is installed.") create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.") confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sale order is confirmed.", oldname="date_confirm") user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', default=lambda self: self.env.user) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, index=True, track_visibility='always') partner_invoice_id = fields.Many2one('res.partner', string='Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order.") partner_shipping_id = fields.Many2one('res.partner', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order.") pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order.") currency_id = fields.Many2one("res.currency", related='pricelist_id.currency_id', string="Currency", readonly=True, required=True) project_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order.", copy=False) order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) invoice_count = fields.Integer(string='# of Invoices', compute='_get_invoiced', readonly=True) invoice_ids = fields.Many2many("account.invoice", string='Invoices', compute="_get_invoiced", readonly=True, copy=False) invoice_status = fields.Selection([ ('upselling', 'Upselling Opportunity'), ('invoiced', 'Fully Invoiced'), ('to invoice', 'To Invoice'), ('no', 'Nothing to Invoice') ], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, default='no') note = fields.Text('Terms and conditions', default=_default_note) amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all', track_visibility='always') amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all', track_visibility='always') payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', oldname='payment_term') fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position') company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order')) team_id = fields.Many2one('crm.team', 'Sales Team', change_default=True, default=_get_default_team, oldname='section_id') procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') @api.model def _get_customer_lead(self, product_tmpl_id): return False @api.multi def button_dummy(self): return True @api.multi def unlink(self): for order in self: if order.state not in ('draft', 'cancel'): raise UserError(_('You can not delete a sent quotation or a sales order! Try to cancel it before.')) return super(SaleOrder, self).unlink() @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'sale': return 'sale.mt_order_confirmed' elif 'state' in init_values and self.state == 'sent': return 'sale.mt_order_sent' return super(SaleOrder, self)._track_subtype(init_values) @api.multi @api.onchange('partner_shipping_id') def onchange_partner_shipping_id(self): """ Trigger the change of fiscal position when the shipping address is modified. """ fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, self.partner_shipping_id.id) if fiscal_position: self.fiscal_position_id = fiscal_position return {} @api.multi @api.onchange('partner_id') def onchange_partner_id(self): """ Update the following fields when the partner is changed: - Pricelist - Payment term - Invoice address - Delivery address """ if not self.partner_id: self.update({ 'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term_id': False, 'fiscal_position_id': False, }) return addr = self.partner_id.address_get(['delivery', 'invoice']) values = { 'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False, 'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False, 'partner_invoice_id': addr['invoice'], 'partner_shipping_id': addr['delivery'], 'note': self.with_context(lang=self.partner_id.lang).env.user.company_id.sale_note, } if self.partner_id.user_id: values['user_id'] = self.partner_id.user_id.id if self.partner_id.team_id: values['team_id'] = self.partner_id.team_id.id self.update(values) @api.onchange('partner_id') def onchange_partner_id_warning(self): if not self.partner_id: return warning = {} title = False message = False partner = self.partner_id # If partner has no warning, check its company if partner.sale_warn == 'no-message' and partner.parent_id: partner = partner.parent_id if partner.sale_warn != 'no-message': # Block if partner only has warning but parent company is blocked if partner.sale_warn != 'block' and partner.parent_id and partner.parent_id.sale_warn == 'block': partner = partner.parent_id title = _("Warning for %s") % partner.name message = partner.sale_warn_msg warning = { 'title': title, 'message': message, } if partner.sale_warn == 'block': self.update({'partner_id': False, 'partner_invoice_id': False, 'partner_shipping_id': False, 'pricelist_id': False}) return {'warning': warning} if warning: return {'warning': warning} @api.model def create(self, vals): if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or 'New' # Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): partner = self.env['res.partner'].browse(vals.get('partner_id')) addr = partner.address_get(['delivery', 'invoice']) vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) vals['partner_shipping_id'] = vals.setdefault('partner_shipping_id', addr['delivery']) vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist and partner.property_product_pricelist.id) result = super(SaleOrder, self).create(vals) return result @api.multi def _prepare_invoice(self): """ Prepare the dict of values to create the new invoice for a sales order. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). """ self.ensure_one() journal_id = self.env['account.invoice'].default_get(['journal_id'])['journal_id'] if not journal_id: raise UserError(_('Please define an accounting sale journal for this company.')) invoice_vals = { 'name': self.client_order_ref or '', 'origin': self.name, 'type': 'out_invoice', 'account_id': self.partner_invoice_id.property_account_receivable_id.id, 'partner_id': self.partner_invoice_id.id, 'journal_id': journal_id, 'currency_id': self.pricelist_id.currency_id.id, 'comment': self.note, 'payment_term_id': self.payment_term_id.id, 'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id, 'company_id': self.company_id.id, 'user_id': self.user_id and self.user_id.id, 'team_id': self.team_id.id } return invoice_vals @api.multi def print_quotation(self): self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'}) return self.env['report'].get_action(self, 'sale.report_saleorder') @api.multi def action_view_invoice(self): invoice_ids = self.mapped('invoice_ids') imd = self.env['ir.model.data'] action = imd.xmlid_to_object('account.action_invoice_tree1') list_view_id = imd.xmlid_to_res_id('account.invoice_tree') form_view_id = imd.xmlid_to_res_id('account.invoice_form') result = { 'name': action.name, 'help': action.help, 'type': action.type, 'views': [[list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'kanban'], [False, 'calendar'], [False, 'pivot']], 'target': action.target, 'context': action.context, 'res_model': action.res_model, } if len(invoice_ids) > 1: result['domain'] = "[('id','in',%s)]" % invoice_ids.ids elif len(invoice_ids) == 1: result['views'] = [(form_view_id, 'form')] result['res_id'] = invoice_ids.ids[0] else: result = {'type': 'ir.actions.act_window_close'} return result @api.multi def action_invoice_create(self, grouped=False, final=False): """ Create the invoice associated to the SO. :param grouped: if True, invoices are grouped by SO id. If False, invoices are grouped by (partner, currency) :param final: if True, refunds will be generated if necessary :returns: list of created invoices """ inv_obj = self.env['account.invoice'] precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') invoices = {} for order in self: group_key = order.id if grouped else (order.partner_id.id, order.currency_id.id) for line in order.order_line.sorted(key=lambda l: l.qty_to_invoice < 0): if float_is_zero(line.qty_to_invoice, precision_digits=precision): continue if group_key not in invoices: inv_data = order._prepare_invoice() invoice = inv_obj.create(inv_data) invoices[group_key] = invoice elif group_key in invoices: vals = {} if order.name not in invoices[group_key].origin.split(', '): vals['origin'] = invoices[group_key].origin + ', ' + order.name if order.client_order_ref and order.client_order_ref not in invoices[group_key].name.split(', '): vals['name'] = invoices[group_key].name + ', ' + order.client_order_ref invoices[group_key].write(vals) if line.qty_to_invoice > 0: line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice) elif line.qty_to_invoice < 0 and final: line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice) for invoice in invoices.values(): if not invoice.invoice_line_ids: raise UserError(_('There is no invoicable line.')) # If invoice is negative, do a refund invoice instead if invoice.amount_untaxed < 0: invoice.type = 'out_refund' for line in invoice.invoice_line_ids: line.quantity = -line.quantity # Use additional field helper function (for account extensions) for line in invoice.invoice_line_ids: line._set_additional_fields(invoice) # Necessary to force computation of taxes. In account_invoice, they are triggered # by onchanges, which are not triggered when doing a create. invoice.compute_taxes() return [inv.id for inv in invoices.values()] @api.multi def action_draft(self): orders = self.filtered(lambda s: s.state in ['cancel', 'sent']) orders.write({ 'state': 'draft', 'procurement_group_id': False, }) orders.mapped('order_line').mapped('procurement_ids').write({'sale_line_id': False}) @api.multi def action_cancel(self): self.write({'state': 'cancel'}) @api.multi def action_quotation_send(self): ''' This function opens a window to compose an email, with the edi sale template message loaded by default ''' self.ensure_one() ir_model_data = self.env['ir.model.data'] try: template_id = ir_model_data.get_object_reference('sale', 'email_template_edi_sale')[1] except ValueError: template_id = False try: compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1] except ValueError: compose_form_id = False ctx = dict() ctx.update({ 'default_model': 'sale.order', 'default_res_id': self.ids[0], 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment', 'mark_so_as_sent': True }) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form_id, 'form')], 'view_id': compose_form_id, 'target': 'new', 'context': ctx, } @api.multi def force_quotation_send(self): for order in self: email_act = order.action_quotation_send() if email_act and email_act.get('context'): email_ctx = email_act['context'] email_ctx.update(default_email_from=order.company_id.email) order.with_context(email_ctx).message_post_with_template(email_ctx.get('default_template_id')) return True @api.multi def action_done(self): self.write({'state': 'done'}) @api.model def _prepare_procurement_group(self): return {'name': self.name} @api.multi def action_confirm(self): for order in self: order.state = 'sale' order.confirmation_date = fields.Datetime.now() if self.env.context.get('send_email'): self.force_quotation_send() order.order_line._action_procurement_create() if not order.project_id: for line in order.order_line: if line.product_id.invoice_policy == 'cost': order._create_analytic_account() break if self.env['ir.values'].get_default('sale.config.settings', 'auto_done_setting'): self.action_done() return True @api.multi def _create_analytic_account(self, prefix=None): for order in self: name = order.name if prefix: name = prefix + ": " + order.name analytic = self.env['account.analytic.account'].create({ 'name': name, 'code': order.client_order_ref, 'company_id': order.company_id.id, 'partner_id': order.partner_id.id }) order.project_id = analytic @api.multi def _notification_group_recipients(self, message, recipients, done_ids, group_data): group_user = self.env.ref('base.group_user') for recipient in recipients: if recipient.id in done_ids: continue if not recipient.user_ids: group_data['partner'] |= recipient else: group_data['user'] |= recipient done_ids.add(recipient.id) return super(SaleOrder, self)._notification_group_recipients(message, recipients, done_ids, group_data) @api.multi def order_lines_layouted(self): """ Returns this order lines classified by sale_layout_category and separated in pages according to the category pagebreaks. Used to render the report. """ self.ensure_one() report_pages = [[]] for category, lines in groupby(self.order_line, lambda l: l.layout_category_id): # If last added category induced a pagebreak, this one will be on a new page if report_pages[-1] and report_pages[-1][-1]['pagebreak']: report_pages.append([]) # Append category to current report page report_pages[-1].append({ 'name': category and category.name or 'Uncategorized', 'subtotal': category and category.subtotal, 'pagebreak': category and category.pagebreak, 'lines': list(lines) }) return report_pages
class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' _description = 'Analytic Line' _order = 'date desc' product_uom_id = fields.Many2one('product.uom', string='Unit of Measure') product_id = fields.Many2one('product.product', string='Product') general_account_id = fields.Many2one('account.account', string='Financial Account', ondelete='restrict', related='move_id.account_id', store=True, domain=[('deprecated', '=', False)]) move_id = fields.Many2one('account.move.line', string='Move Line', ondelete='cascade', index=True) journal_id = fields.Many2one('account.analytic.journal', string='Analytic Journal', required=True, ondelete='restrict', index=True) code = fields.Char(size=8) ref = fields.Char(string='Ref.') currency_id = fields.Many2one( 'res.currency', related='move_id.currency_id', string='Account Currency', store=True, help="The related account currency if not equal to the company one.", readonly=True) amount_currency = fields.Monetary( related='move_id.amount_currency', store=True, help= "The amount expressed in the related account currency if not equal to the company one.", readonly=True) partner_id = fields.Many2one('res.partner', related='account_id.partner_id', string='Partner', store=True) # Compute the cost based on the price type define into company # property_valuation_price_type property @api.v7 def on_change_unit_amount(self, cr, uid, id, prod_id, quantity, company_id, unit=False, journal_id=False, context=None): if context is None: context = {} if not journal_id: j_ids = self.pool.get('account.analytic.journal').search( cr, uid, [('type', '=', 'purchase')]) journal_id = j_ids and j_ids[0] or False if not journal_id: return {} product_obj = self.pool.get('product.product') analytic_journal_obj = self.pool.get('account.analytic.journal') product_price_type_obj = self.pool.get('product.price.type') product_uom_obj = self.pool.get('product.uom') j_id = analytic_journal_obj.browse(cr, uid, journal_id, context=context) prod = product_obj.browse(cr, uid, prod_id, context=context) result = 0.0 if prod_id: unit_obj = False if unit: unit_obj = product_uom_obj.browse(cr, uid, unit, context=context) if not unit_obj or prod.uom_id.category_id.id != unit_obj.category_id.id: unit = prod.uom_id.id if j_id.type == 'purchase': if not unit_obj or prod.uom_po_id.category_id.id != unit_obj.category_id.id: unit = prod.uom_po_id.id if j_id.type <> 'sale': a = prod.property_account_expense_id.id if not a: a = prod.categ_id.property_account_expense_categ_id.id else: a = prod.property_account_income_id.id if not a: a = prod.categ_id.property_account_income_categ_id.id flag = False # Compute based on pricetype product_price_type_ids = product_price_type_obj.search( cr, uid, [('field', '=', 'standard_price')], context=context) pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0] if journal_id: journal = analytic_journal_obj.browse(cr, uid, journal_id, context=context) if journal.type == 'sale': product_price_type_ids = product_price_type_obj.search( cr, uid, [('field', '=', 'list_price')], context=context) if product_price_type_ids: pricetype = product_price_type_obj.browse( cr, uid, product_price_type_ids, context=context)[0] # Take the company currency as the reference one if pricetype.field == 'list_price': flag = True ctx = context.copy() if unit: # price_get() will respect a 'uom' in its context, in order # to return a default price for those units ctx['uom'] = unit amount_unit = prod.price_get(pricetype.field, context=ctx) if amount_unit: amount_unit = amount_unit[prod.id] else: amount_unit = 0.0 amount = amount_unit * quantity or 0.0 cur_record = self.browse(cr, uid, id, context=context) currency = cur_record.exists( ) and cur_record.currency_id or prod.company_id.currency_id result = round(amount, currency.decimal_places) if not flag: result *= -1 return { 'value': { 'amount': result, 'general_account_id': a, 'product_uom_id': unit } } @api.v8 @api.onchange('product_id', 'product_uom_id') def on_change_unit_amount(self): product_price_type_obj = self.env['product.price.type'] journal_id = self.journal_id if not journal_id: journal_id = self.env['account.analytic.journal'].search( [('type', '=', 'purchase')], limit=1) if not journal_id or not self.product_id: return {} result = 0.0 if self.product_id: unit = self.product_uom_id.id if not self.product_uom_id or self.product_id.uom_id.category_id.id != self.product_uom_id.category_id.id: unit = self.product_id.uom_id.id if journal_id.type == 'purchase': if not self.product_uom_id or self.product_id.uom_po_id.category_id.id != self.product_uom_id.category_id.id: unit = self.product_id.uom_po_id.id if journal_id.type != 'sale': account = self.product_id.property_account_expense_id.id or self.product_id.categ_id.property_account_expense_categ_id.id if not account: raise UserError(_('There is no expense account defined ' \ 'for this product: "%s" (id:%d).') % \ (self.product_id.name, self.product_id.id,)) else: account = self.product_id.property_account_income_id.id or self.product_id.categ_id.property_account_income_categ_id.id if not account: raise UserError(_('There is no income account defined ' \ 'for this product: "%s" (id:%d).') % \ (self.product_id.name, self.product_id.id,)) # Compute based on pricetype if journal_id.type == 'sale': pricetype = product_price_type_obj.search( [('field', '=', 'list_price')], limit=1) else: pricetype = product_price_type_obj.search( [('field', '=', 'standard_price')], limit=1) ctx = dict(self._context or {}) if unit: # price_get() will respect a 'uom' in its context, in order # to return a default price for those units ctx['uom'] = unit amount_unit = self.product_id.with_context(ctx).price_get( pricetype.field)[self.product_id.id] amount = amount_unit * self.unit_amount or 0.0 result = round(amount, self.currency_id.decimal_places) if pricetype.field != 'list_price': result *= -1 self.amount = result self.general_account_id = account self.product_uom_id = unit @api.model def view_header_get(self, view_id, view_type): context = (self._context or {}) header = False if context.get('account_id', False): analytic_account = self.env['account.analytic.account'].search( [('id', '=', context['account_id'])], limit=1) header = _('Entries: ') + (analytic_account.name or '') return header
class AccountBankStatement(models.Model): @api.one @api.depends('line_ids', 'balance_start', 'line_ids.amount', 'balance_end_real') def _end_balance(self): self.total_entry_encoding = sum([line.amount for line in self.line_ids]) self.balance_end = self.balance_start + self.total_entry_encoding self.difference = self.balance_end_real - self.balance_end @api.multi def _is_difference_zero(self): for bank_stmt in self: bank_stmt.is_difference_zero = float_is_zero(bank_stmt.difference, precision_digits=bank_stmt.currency_id.decimal_places) @api.one @api.depends('journal_id') def _compute_currency(self): self.currency_id = self.journal_id.currency_id or self.company_id.currency_id @api.one @api.depends('line_ids.journal_entry_ids') def _check_lines_reconciled(self): self.all_lines_reconciled = all([line.journal_entry_ids.ids or line.account_id.id for line in self.line_ids]) @api.model def _default_journal(self): journal_type = self.env.context.get('journal_type', False) company_id = self.env['res.company']._company_default_get('account.bank.statement').id if journal_type: journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)]) if journals: return journals[0] return False @api.multi def _get_opening_balance(self, journal_id): last_bnk_stmt = self.search([('journal_id', '=', journal_id)], limit=1) if last_bnk_stmt: return last_bnk_stmt.balance_end return 0 @api.multi def _set_opening_balance(self, journal_id): self.balance_start = self._get_opening_balance(journal_id) @api.model def _default_opening_balance(self): #Search last bank statement and set current opening balance as closing balance of previous one journal_id = self._context.get('default_journal_id', False) or self._context.get('journal_id', False) if journal_id: return self._get_opening_balance(journal_id) return 0 _name = "account.bank.statement" _description = "Bank Statement" _order = "date desc, id desc" _inherit = ['mail.thread'] name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True) reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)") date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, select=True, copy=False, default=fields.Date.context_today) date_done = fields.Datetime(string="Closed On") balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}, default=_default_opening_balance) balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]}) state = fields.Selection([('open', 'New'), ('confirm', 'Validated')], string='Status', required=True, readonly=True, copy=False, default='open') currency_id = fields.Many2one('res.currency', compute='_compute_currency', oldname='currency', string="Currency") journal_id = fields.Many2one('account.journal', string='Journal', required=True, states={'confirm': [('readonly', True)]}, default=_default_journal) journal_type = fields.Selection(related='journal_id.type', help="Technical field used for usability purposes") company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, default=lambda self: self.env['res.company']._company_default_get('account.bank.statement')) total_entry_encoding = fields.Monetary('Transactions Subtotal', compute='_end_balance', store=True, help="Total of transaction lines.") balance_end = fields.Monetary('Computed Balance', compute='_end_balance', store=True, help='Balance as calculated based on Opening Balance and transaction lines') difference = fields.Monetary(compute='_end_balance', store=True, help="Difference between the computed ending balance and the specified ending balance.") line_ids = fields.One2many('account.bank.statement.line', 'statement_id', string='Statement lines', states={'confirm': [('readonly', True)]}, copy=True) move_line_ids = fields.One2many('account.move.line', 'statement_id', string='Entry lines', states={'confirm': [('readonly', True)]}) all_lines_reconciled = fields.Boolean(compute='_check_lines_reconciled') user_id = fields.Many2one('res.users', string='Responsible', required=False, default=lambda self: self.env.user) cashbox_start_id = fields.Many2one('account.bank.statement.cashbox', string="Starting Cashbox") cashbox_end_id = fields.Many2one('account.bank.statement.cashbox', string="Ending Cashbox") is_difference_zero = fields.Boolean(compute='_is_difference_zero', string='Is zero', help="Check if difference is zero.") @api.onchange('journal_id') def onchange_journal_id(self): self._set_opening_balance(self.journal_id.id) @api.multi def _balance_check(self): for stmt in self: if not stmt.currency_id.is_zero(stmt.difference): if stmt.journal_type == 'cash': if stmt.difference < 0.0: account = stmt.journal_id.loss_account_id name = _('Loss') else: # statement.difference > 0.0 account = stmt.journal_id.profit_account_id name = _('Profit') if not account: raise UserError(_('There is no account defined on the journal %s for %s involved in a cash difference.') % (stmt.journal_id.name, name)) values = { 'statement_id': stmt.id, 'account_id': account.id, 'amount': stmt.difference, 'name': _("Cash difference observed during the counting (%s)") % name, } self.env['account.bank.statement.line'].create(values) else: balance_end_real = formatLang(self.env, stmt.balance_end_real, currency_obj=stmt.currency_id) balance_end = formatLang(self.env, stmt.balance_end, currency_obj=stmt.currency_id) raise UserError(_('The ending balance is incorrect !\nThe expected balance (%s) is different from the computed one. (%s)') % (balance_end_real, balance_end)) return True @api.model def create(self, vals): if not vals.get('name'): journal_id = vals.get('journal_id', self._context.get('default_journal_id', False)) journal = self.env['account.journal'].browse(journal_id) vals['name'] = journal.sequence_id.with_context(ir_sequence_date=vals.get('date')).next_by_id() return super(AccountBankStatement, self).create(vals) @api.multi def unlink(self): for statement in self: if statement.state != 'open': raise UserError(_('In order to delete a bank statement, you must first cancel it to delete related journal items.')) # Explicitly unlink bank statement lines so it will check that the related journal entries have been deleted first statement.line_ids.unlink() return super(AccountBankStatement, self).unlink() @api.multi def open_cashbox_id(self): context = dict(self.env.context or {}) if context.get('cashbox_id'): context['active_id'] = self.id return { 'name': _('Cash Control'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.bank.statement.cashbox', 'view_id': self.env.ref('account.view_account_bnk_stmt_cashbox').id, 'type': 'ir.actions.act_window', 'res_id': self.env.context.get('cashbox_id'), 'context': context, 'target': 'new' } @api.multi def button_cancel(self): for statement in self: if any(line.journal_entry_ids.ids for line in statement.line_ids): raise UserError(_('A statement cannot be canceled when its lines are reconciled.')) self.state = 'open' @api.multi def check_confirm_bank(self): if self.journal_type == 'cash' and not self.currency_id.is_zero(self.difference): action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_view_account_bnk_stmt_check') if action_rec: action = action_rec.read([])[0] return action return self.button_confirm_bank() @api.multi def button_confirm_bank(self): self._balance_check() statements = self.filtered(lambda r: r.state == 'open') for statement in statements: moves = self.env['account.move'] for st_line in statement.line_ids: if st_line.account_id and not st_line.journal_entry_ids.ids: st_line.fast_counterpart_creation() elif not st_line.journal_entry_ids.ids: raise UserError(_('All the account entries lines must be processed in order to close the statement.')) moves = (moves | st_line.journal_entry_ids) if moves: moves.post() statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,)) statements.link_bank_to_partner() statements.write({'state': 'confirm', 'date_done': time.strftime("%Y-%m-%d %H:%M:%S")}) @api.multi def button_journal_entries(self): context = dict(self._context or {}) context['journal_id'] = self.journal_id.id return { 'name': _('Journal Items'), 'view_type': 'form', 'view_mode': 'tree', 'res_model': 'account.move.line', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('statement_id', 'in', self.ids)], 'context': context, } @api.multi def button_open(self): """ Changes statement state to Running.""" for statement in self: if not statement.name: context = {'ir_sequence_date', statement.date} if statement.journal_id.sequence_id: st_number = statement.journal_id.sequence_id.with_context(context).next_by_id() else: SequenceObj = self.env['ir.sequence'] st_number = SequenceObj.with_context(context).next_by_code('account.bank.statement') statement.name = st_number statement.state = 'open' @api.multi def reconciliation_widget_preprocess(self): """ Get statement lines of the specified statements or all unreconciled statement lines and try to automatically reconcile them / find them a partner. Return ids of statement lines left to reconcile and other data for the reconciliation widget. """ statements = self bsl_obj = self.env['account.bank.statement.line'] # NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on, # this is why we filter out statements lines where account_id is set st_lines_filter = [('journal_entry_ids', '=', False), ('account_id', '=', False)] if statements: st_lines_filter += [('statement_id', 'in', statements.ids)] # Try to automatically reconcile statement lines automatic_reconciliation_entries = [] st_lines_left = self.env['account.bank.statement.line'] for st_line in bsl_obj.search(st_lines_filter): res = st_line.auto_reconcile() if not res: st_lines_left = (st_lines_left | st_line) else: automatic_reconciliation_entries.append(res.ids) # Try to set statement line's partner for st_line in st_lines_left: if st_line.name and not st_line.partner_id: additional_domain = [('ref', '=', st_line.name)] match_recs = st_line.get_move_lines_for_reconciliation(limit=1, additional_domain=additional_domain, overlook_partner=True) if match_recs and match_recs[0].partner_id: st_line.write({'partner_id': match_recs[0].partner_id.id}) # Collect various informations for the reconciliation widget notifications = [] num_auto_reconciled = len(automatic_reconciliation_entries) if num_auto_reconciled > 0: auto_reconciled_message = num_auto_reconciled > 1 \ and _("%d transactions were automatically reconciled.") % num_auto_reconciled \ or _("1 transaction was automatically reconciled.") notifications += [{ 'type': 'info', 'message': auto_reconciled_message, 'details': { 'name': _("Automatically reconciled items"), 'model': 'account.move', 'ids': automatic_reconciliation_entries } }] lines = [] for el in statements: lines.extend(el.line_ids.ids) lines = list(set(lines)) return { 'st_lines_ids': st_lines_left.ids, 'notifications': notifications, 'statement_name': len(statements) == 1 and statements[0].name or False, 'num_already_reconciled_lines': statements and bsl_obj.search_count([('journal_entry_ids', '!=', False), ('id', 'in', lines)]) or 0, } @api.multi def link_bank_to_partner(self): for statement in self: for st_line in statement.line_ids: if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id != st_line.partner_id: st_line.bank_account_id.partner_id = st_line.partner_id
class PurchaseOrderLine(models.Model): _name = 'purchase.order.line' _description = 'Purchase Order Line' @api.depends('product_qty', 'price_unit', 'taxes_id') def _compute_amount(self): for line in self: taxes = line.taxes_id.compute_all(line.price_unit, line.order_id.currency_id, line.product_qty, product=line.product_id, partner=line.order_id.partner_id) line.update({ 'price_tax': taxes['total_included'] - taxes['total_excluded'], 'price_total': taxes['total_included'], 'price_subtotal': taxes['total_excluded'], }) @api.depends('invoice_lines.invoice_id.state') def _compute_qty_invoiced(self): for line in self: qty = 0.0 for inv_line in line.invoice_lines: if inv_line.invoice_id.state not in ['cancel']: qty += inv_line.uom_id._compute_qty_obj(inv_line.uom_id, inv_line.quantity, line.product_uom) line.qty_invoiced = qty @api.depends('order_id.state', 'move_ids.state') def _compute_qty_received(self): productuom = self.env['product.uom'] for line in self: if line.order_id.state not in ['purchase', 'done']: line.qty_received = 0.0 continue if line.product_id.type not in ['consu', 'product']: line.qty_received = line.product_qty continue bom_delivered = self.sudo()._get_bom_delivered(line.sudo()) if bom_delivered and any(bom_delivered.values()): total = line.product_qty elif bom_delivered: total = 0.0 else: total = 0.0 for move in line.move_ids: if move.state == 'done': if move.product_uom != line.product_uom: total += productuom._compute_qty_obj(move.product_uom, move.product_uom_qty, line.product_uom) else: total += move.product_uom_qty line.qty_received = total def _get_bom_delivered(self, line): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') bom_delivered = {} # There is no dependencies between purchase and mrp if 'mrp.bom' in self.env: # In the case of a kit, we need to check if all components are shipped. We use a all or # nothing policy. A product can have several BoMs, we don't know which one was used when the # delivery was created. for bom in line.product_id.product_tmpl_id.bom_ids: if bom.type != 'phantom': continue bom_delivered[bom.id] = False product_uom_qty_bom = self.env['product.uom']._compute_qty_obj(line.product_uom, line.product_qty, bom.product_uom) bom_exploded = self.env['mrp.bom']._bom_explode(bom, line.product_id, product_uom_qty_bom)[0] for bom_line in bom_exploded: qty = 0.0 for move in line.move_ids: if move.state == 'done' and move.product_id.id == bom_line.get('product_id', False): qty += self.env['product.uom']._compute_qty(move.product_uom.id, move.product_uom_qty, bom_line['product_uom']) if float_compare(qty, bom_line['product_qty'], precision_digits=precision) < 0: bom_delivered[bom.id] = False break else: bom_delivered[bom.id] = True return bom_delivered name = fields.Text(string='Description', required=True) product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True) taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True) product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True) move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False) price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price')) price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True) order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade') account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', domain=[('account_type', '=', 'normal')]) company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True) state = fields.Selection(related='order_id.state', stored=True) invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Invoice Lines", readonly=True, copy=False) # Replace by invoiced Qty qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", store=True) qty_received = fields.Float(compute='_compute_qty_received', string="Received Qty", store=True) partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True) currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True) procurement_ids = fields.One2many('procurement.order', 'purchase_line_id', string='Associated Procurements', copy=False) @api.multi def _get_stock_move_price_unit(self): self.ensure_one() line = self[0] order = line.order_id price_unit = line.price_unit if line.taxes_id: price_unit = line.taxes_id.with_context(round=False).compute_all(price_unit, currency=line.order_id.currency_id, quantity=1.0)['total_excluded'] if line.product_uom.id != line.product_id.uom_id.id: price_unit *= line.product_uom.factor / line.product_id.uom_id.factor if order.currency_id != order.company_id.currency_id: price_unit = order.currency_id.compute(price_unit, order.company_id.currency_id, round=False) return price_unit @api.multi def _create_stock_moves(self, picking): moves = self.env['stock.move'] done = self.env['stock.move'].browse() for line in self: price_unit = line._get_stock_move_price_unit() template = { 'name': line.name or '', 'product_id': line.product_id.id, 'product_uom': line.product_uom.id, 'date': line.order_id.date_order, 'date_expected': line.date_planned, 'location_id': line.order_id.partner_id.property_stock_supplier.id, 'location_dest_id': line.order_id._get_destination_location(), 'picking_id': picking.id, 'partner_id': line.order_id.dest_address_id.id, 'move_dest_id': False, 'state': 'draft', 'purchase_line_id': line.id, 'company_id': line.order_id.company_id.id, 'price_unit': price_unit, 'picking_type_id': line.order_id.picking_type_id.id, 'group_id': line.order_id.group_id.id, 'procurement_id': False, 'origin': line.order_id.name, 'route_ids': line.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in line.order_id.picking_type_id.warehouse_id.route_ids])] or [], 'warehouse_id':line.order_id.picking_type_id.warehouse_id.id, } # Fullfill all related procurements with this po line diff_quantity = line.product_qty for procurement in line.procurement_ids: procurement_qty = procurement.product_uom._compute_qty_obj(procurement.product_uom, procurement.product_qty, line.product_uom) tmp = template.copy() tmp.update({ 'product_uom_qty': min(procurement_qty, diff_quantity), 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination 'procurement_id': procurement.id, 'propagate': procurement.rule_id.propagate, }) done += moves.create(tmp) diff_quantity -= min(procurement_qty, diff_quantity) if float_compare(diff_quantity, 0.0, precision_rounding=line.product_uom.rounding) > 0: template['product_uom_qty'] = diff_quantity done += moves.create(template) return done @api.multi def unlink(self): for line in self: if line.order_id.state in ['approved', 'done']: raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,)) for proc in line.procurement_ids: proc.message_post(body=_('Purchase order line deleted.')) line.procurement_ids.filtered(lambda r: r.state != 'cancel').write({'state': 'exception'}) return super(PurchaseOrderLine, self).unlink() @api.model def _get_date_planned(self, seller, po=False): """Return the datetime value to use as Schedule Date (``date_planned``) for PO Lines that correspond to the given product.seller_ids, when ordered at `date_order_str`. :param browse_record | False product: product.product, used to determine delivery delay thanks to the selected seller field (if False, default delay = 0) :param browse_record | False po: purchase.order, necessary only if the PO line is not yet attached to a PO. :rtype: datetime :return: desired Schedule Date for the PO line """ date_order = po.date_order if po else self.order_id.date_order if date_order: return datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=seller.delay if seller else 0) else: return datetime.today() + relativedelta(days=seller.delay if seller else 0) @api.onchange('product_id') def onchange_product_id(self): result = {} if not self.product_id: return result # Reset date, price and quantity since _onchange_quantity will provide default values self.date_planned = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT) self.price_unit = self.product_qty = 0.0 self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]} product_lang = self.product_id.with_context({ 'lang': self.partner_id.lang, 'partner_id': self.partner_id.id, }) self.name = product_lang.display_name if product_lang.description_purchase: self.name += '\n' + product_lang.description_purchase fpos = self.order_id.fiscal_position_id if self.env.uid == SUPERUSER_ID: company_id = self.env.user.company_id.id self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id.id == company_id)) else: self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id) self._suggest_quantity() self._onchange_quantity() return result @api.onchange('product_qty', 'product_uom') def _onchange_quantity(self): if not self.product_id: return seller = self.product_id._select_seller( self.product_id, partner_id=self.partner_id, quantity=self.product_qty, date=self.order_id.date_order and self.order_id.date_order[:10], uom_id=self.product_uom) if seller or not self.date_planned: self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT) if not seller: return price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, self.taxes_id) if seller else 0.0 if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id: price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id) if seller and self.product_uom and seller.product_uom != self.product_uom: price_unit = self.env['product.uom']._compute_price(seller.product_uom.id, price_unit, to_uom_id=self.product_uom.id) self.price_unit = price_unit def _suggest_quantity(self): ''' Suggest a minimal quantity based on the seller ''' if not self.product_id: return seller_min_qty = self.product_id.seller_ids\ .filtered(lambda r: r.name == self.order_id.partner_id)\ .sorted(key=lambda r: r.min_qty) if seller_min_qty: self.product_qty = seller_min_qty[0].min_qty or 1.0 self.product_uom = seller_min_qty[0].product_uom else: self.product_qty = 1.0
class AccountInvoice(models.Model): """ about name_get and display name: * in this model name_get and name_search are re-defined so we overwrite them * we add display_name to replace name field use, we add with search funcion. This field is used then for name_get and name_search Acccoding this https://www.odoo.com/es_ES/forum/ayuda-1/question/ how-to-override-name-get-method-in-new-api-61228 we should modify name_get, we do that by creating a helper display_name field and also overwriting name_get to use it """ _inherit = "account.invoice" _order = "date_invoice desc, document_number desc, number desc, id desc" # _order = "document_number desc, number desc, id desc" report_amount_tax = fields.Monetary( string='Tax', compute='_compute_report_amount_and_taxes') report_amount_untaxed = fields.Monetary( string='Untaxed Amount', compute='_compute_report_amount_and_taxes') report_tax_line_ids = fields.One2many( compute="_compute_report_amount_and_taxes", comodel_name='account.invoice.tax', string='Taxes') available_journal_document_type_ids = fields.Many2many( 'account.journal.document.type', compute='get_available_journal_document_types', string='Available Journal Document Types', ) journal_document_type_id = fields.Many2one( 'account.journal.document.type', 'Document Type', readonly=True, ondelete='restrict', copy=False, auto_join=True, states={'draft': [('readonly', False)]}) # we add this fields so we can search, group and analyze by this one document_type_id = fields.Many2one( related='journal_document_type_id.document_type_id', copy=False, readonly=True, store=True, auto_join=True, ) document_sequence_id = fields.Many2one( related='journal_document_type_id.sequence_id', readonly=True, ) document_number = fields.Char( string='Document Number', copy=False, readonly=True, states={'draft': [('readonly', False)]}, track_visibility='onchange', ) display_name = fields.Char( compute='_get_display_name', string='Document Reference', ) next_number = fields.Integer( compute='_get_next_number', string='Next Number', ) use_documents = fields.Boolean(related='journal_id.use_documents', string='Use Documents?', readonly=True) localization = fields.Selection( related='company_id.localization', readonly=True, ) document_type_internal_type = fields.Selection( related='document_type_id.internal_type', readonly=True, ) @api.multi def _get_tax_amount_by_group(self): """ we can not inherit because of function design, we overwrite """ self.ensure_one() res = {} currency = self.currency_id or self.company_id.currency_id for line in self.report_tax_line_ids: res.setdefault(line.tax_id.tax_group_id, 0.0) res[line.tax_id.tax_group_id] += line.amount res = sorted(res.items(), key=lambda l: l[0].sequence) res = map( lambda l: (l[0].name, formatLang(self.env, l[1], currency_obj=currency)), res) return res @api.depends('amount_untaxed', 'amount_tax', 'tax_line_ids', 'document_type_id') def _compute_report_amount_and_taxes(self): for invoice in self: taxes_included = (invoice.document_type_id and invoice.document_type_id.get_taxes_included() or False) if not taxes_included: report_amount_tax = invoice.amount_tax report_amount_untaxed = invoice.amount_untaxed not_included_taxes = invoice.tax_line_ids else: included_taxes = invoice.tax_line_ids.filtered( lambda x: x.tax_id in taxes_included) not_included_taxes = (invoice.tax_line_ids - included_taxes) report_amount_tax = sum(not_included_taxes.mapped('amount')) report_amount_untaxed = invoice.amount_untaxed + sum( included_taxes.mapped('amount')) invoice.report_amount_tax = report_amount_tax invoice.report_amount_untaxed = report_amount_untaxed invoice.report_tax_line_ids = not_included_taxes @api.multi @api.depends( 'journal_id.sequence_id.number_next_actual', 'journal_document_type_id.sequence_id.number_next_actual', ) def _get_next_number(self): """ show next number only for invoices without number and on draft state """ for invoice in self.filtered( lambda x: not x.display_name and x.state == 'draft'): if invoice.use_documents: sequence = invoice.journal_document_type_id.sequence_id elif (invoice.type in ['out_refund', 'in_refund'] and invoice.journal_id.refund_sequence): sequence = invoice.journal_id.refund_sequence_id else: sequence = invoice.journal_id.sequence_id # we must check if sequence use date ranges if not sequence.use_date_range: invoice.next_number = sequence.number_next_actual else: dt = fields.Date.today() if self.env.context.get('ir_sequence_date'): dt = self.env.context.get('ir_sequence_date') seq_date = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', sequence.id), ('date_from', '<=', dt), ('date_to', '>=', dt)], limit=1) if not seq_date: seq_date = sequence._create_date_range_seq(dt) invoice.next_number = seq_date.number_next_actual @api.multi def name_get(self): TYPES = { 'out_invoice': _('Invoice'), 'in_invoice': _('Vendor Bill'), 'out_refund': _('Refund'), 'in_refund': _('Vendor Refund'), } result = [] for inv in self: result.append( (inv.id, "%s %s" % (inv.display_name or TYPES[inv.type], inv.name or ''))) return result @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] recs = self.browse() if name: recs = self.search( ['|', ('document_number', '=', name), ('number', '=', name)] + args, limit=limit) if not recs: recs = self.search([('name', operator, name)] + args, limit=limit) return recs.name_get() @api.multi @api.constrains( 'journal_id', 'partner_id', 'journal_document_type_id', ) def _get_document_type(self): """ Como los campos responsible y journal document type no los queremos hacer funcion porque no queremos que sus valores cambien nunca y como con la funcion anterior solo se almacenan solo si se crea desde interfaz, hacemos este hack de constraint para computarlos si no estan computados""" for rec in self: if (not rec.journal_document_type_id and rec.available_journal_document_type_ids): rec.journal_document_type_id = ( rec._get_available_journal_document_types( rec.journal_id, rec.type, rec.partner_id).get('journal_document_type')) @api.multi @api.depends('move_name', 'document_number', 'document_type_id.doc_code_prefix') def _get_display_name(self): """ If move_name then invoice has been validated, then: * If document number and document type, we show them * Else, we show move_name """ # al final no vimos porque necesiamos que este el move name, es util # mostrar igual si existe el numero, por ejemplo si es factura de # proveedor # if self.document_number and self.document_type_id and self.move_name: for rec in self: if rec.document_number and rec.document_type_id: display_name = ("%s%s" % (rec.document_type_id.doc_code_prefix or '', rec.document_number)) else: display_name = rec.move_name rec.display_name = display_name @api.multi def check_use_documents(self): """ check invoices has document class but journal require it (we check all invoices, not only argentinian ones) """ without_doucument_class = self.filtered( lambda r: (not r.document_type_id and r.journal_id.use_documents)) if without_doucument_class: raise UserError( _('Some invoices have a journal that require a document but not ' 'document type has been selected.\n' 'Invoices ids: %s' % without_doucument_class.ids)) @api.multi def get_localization_invoice_vals(self): """ Function to be inherited by different localizations and add custom data to invoice on invoice validation """ self.ensure_one() return {} @api.multi def action_move_create(self): """ We add currency rate on move creation so it can be used by electronic invoice later on action_number """ self.check_use_documents() res = super(AccountInvoice, self).action_move_create() self.set_document_data() return res @api.multi def set_document_data(self): """ If journal document dont have any sequence, then document number must be set on the account.invoice and we use thisone A partir de este metodo no debería haber errores porque el modulo de factura electronica ya habria pedido el cae. Lo ideal sería hacer todo esto antes que se pida el cae pero tampoco se pueden volver a atras los conusmos de secuencias. TODO mejorar esa parte """ # We write document_number field with next invoice number by # document type for invoice in self: _logger.info( 'Setting document data on account.invoice and account.move') journal_document_type = invoice.journal_document_type_id inv_vals = self.get_localization_invoice_vals() if invoice.use_documents: if not invoice.document_number: if not invoice.journal_document_type_id.sequence_id: raise UserError( _('Error!. Please define sequence on the journal ' 'related documents to this invoice or set the ' 'document number.')) document_number = ( journal_document_type.sequence_id.next_by_id()) inv_vals['document_number'] = document_number # for canelled invoice number that still has a document_number # if validated again we use old document_number # also use this for supplier invoices else: document_number = invoice.document_number invoice.move_id.write({ 'document_type_id': (journal_document_type.document_type_id.id), 'document_number': document_number, }) invoice.write(inv_vals) return True @api.multi @api.depends('journal_id', 'partner_id', 'company_id') def get_available_journal_document_types(self): """ This function should only be inherited if you need to add another "depends", for eg, if you need to add a depend on "new_field" you should add: @api.depends('new_field') def _get_available_journal_document_types( self, journal, invoice_type, partner): return super( AccountInvoice, self)._get_available_journal_document_types( journal, invoice_type, partner) """ for invoice in self: res = invoice._get_available_journal_document_types( invoice.journal_id, invoice.type, invoice.partner_id) invoice.available_journal_document_type_ids = res[ 'available_journal_document_types'] invoice.journal_document_type_id = res['journal_document_type'] @api.multi def write(self, vals): """ If someone change the type (for eg from sale order), we update de document type """ inv_type = vals.get('type') # if len(vals) == 1 and vals.get('type'): # podrian pasarse otras cosas ademas del type if inv_type: for rec in self: res = rec._get_available_journal_document_types( rec.journal_id, inv_type, rec.partner_id) vals['journal_document_type_id'] = res[ 'journal_document_type'].id # call write for each inoice super(AccountInvoice, rec).write(vals) return True return super(AccountInvoice, self).write(vals) @api.model def _get_available_journal_document_types(self, journal, invoice_type, partner): """ This function is to be inherited by differents localizations and MUST return a dictionary with two keys: * 'available_journal_document_types': available document types on this invoice * 'journal_document_type': suggested document type on this invoice """ # As default we return every journal document type, and if one exists # then we return the first one as suggested journal_document_types = journal.journal_document_type_ids # if invoice is a refund only show credit_notes, else, not credit note if invoice_type in ['out_refund', 'in_refund']: journal_document_types = journal_document_types.filtered( # lambda x: x.document_type_id.internal_type == 'credit_note') lambda x: x.document_type_id.internal_type in ['credit_note', 'in_document']) else: journal_document_types = journal_document_types.filtered( lambda x: x.document_type_id.internal_type != 'credit_note') journal_document_type = (journal_document_types and journal_document_types[0] or journal_document_types) return { 'available_journal_document_types': journal_document_types, 'journal_document_type': journal_document_type, } @api.multi @api.constrains('document_type_id', 'document_number') @api.onchange('document_type_id', 'document_number') def validate_document_number(self): for rec in self: # if we have a sequence, number is set by sequence and we dont # check this if rec.document_sequence_id: continue document_type = rec.document_type_id if rec.document_type_id: res = document_type.validate_document_number( rec.document_number) if res and res != rec.document_number: rec.document_number = res @api.multi @api.constrains('journal_document_type_id', 'journal_id') def check_journal_document_type_journal(self): for rec in self: if rec.journal_document_type_id and ( rec.journal_document_type_id.journal_id != rec.journal_id): raise Warning( _('El Tipo de Documento elegido "%s" no pertenece al diario' ' "%s". Por favor pruebe elegir otro tipo de documento.' 'Puede refrezcar los tipos de documentos disponibles ' 'cambiando el diario o el partner.') % ((rec.journal_document_type_id.display_name, rec.journal_id.name))) @api.multi @api.constrains('type', 'document_type_id') def check_invoice_type_document_type(self): for rec in self: internal_type = rec.document_type_internal_type invoice_type = rec.type if not internal_type: continue elif internal_type in [ 'debit_note', 'invoice' ] and invoice_type in ['out_refund', 'in_refund']: raise Warning( _('You can not use a %s document type with a refund ' 'invoice') % internal_type) elif internal_type == 'credit_note' and invoice_type in [ 'out_invoice', 'in_invoice' ]: raise Warning( _('You can not use a %s document type with a invoice') % (internal_type)) @api.model def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None): values = super(AccountInvoice, self)._prepare_refund(invoice, date_invoice=date_invoice, date=date, description=description, journal_id=journal_id) refund_journal_document_type_id = self._context.get( 'refund_journal_document_type_id', False) refund_document_number = self._context.get('refund_document_number', False) if refund_journal_document_type_id: values['journal_document_type_id'] = \ refund_journal_document_type_id if refund_document_number: values['document_number'] = refund_document_number return values
class account_register_payments(models.TransientModel): _inherit = "account.register.payments" def _get_register_invoices(self): return self.env['account.invoice'].browse(self._context.get('active_ids')) def _get_register_lines(self, register_ids): registers = [] if register_ids: for register in register_ids: registers.append(register.id) return self.env['account.register.line'].browse(registers) def _set_currency_rate(self): if self.is_force_curr: company_currency = self.journal_id.company_id.currency_id payment_currency = self.currency_id or company_currency self.force_rate = payment_currency.with_context(date=self.payment_date).compute(1.0, company_currency, round=False) self.company_currency_id = company_currency.id else: self.force_rate = 0.0 company_currency_id = fields.Many2one('res.currency', string='Company Currency') is_force_curr = fields.Boolean('Kurs Nego') force_rate = fields.Monetary('Kurs Nego Amount') payment_adm = fields.Selection([ ('cash','Cash'), ('free_transfer','Non Payment Administration Transfer'), ('transfer','Transfer'), #('check','Check/Giro'), #('letter','Letter Credit'), ('cc','Credit Card'), ('dc','Debit Card'), ],string='Payment Adm') card_number = fields.Char('Card Number', size=128, required=False) card_type = fields.Selection([ ('visa','Visa'), ('master','Master'), ('bca','BCA Card'), ('citi','CITI Card'), ('amex','AMEX'), ], string='Card Type', size=128) register_ids = fields.One2many('account.register.line', 'register_id', copy=False, string='Register Invoice') @api.model def default_get(self, fields): rec = super(account_register_payments, self).default_get(fields) context = dict(self._context or {}) active_model = context.get('active_model') active_ids = context.get('active_ids') reg_lines = [] communication = [] for invoice in self.env[active_model].browse(active_ids): if invoice.origin: name = invoice.number +':'+ invoice.origin else: name = invoice.number communication.append(name) reg_lines.append([0, 0, { 'invoice_id': invoice.id, 'name': name, 'amount_total': invoice.amount_total, 'residual': invoice.residual, 'amount_to_pay': invoice.residual, }]) rec.update({ 'register_ids': reg_lines, 'communication': ", ".join(communication), }) return rec @api.onchange('journal_id', 'amount') def _onchange_journal_id(self): if self.journal_id: company_currency = self.journal_id.company_id.currency_id payment_currency = self.currency_id or company_currency if company_currency == payment_currency: self.is_force_curr = False self.force_rate = 0.0 @api.onchange('is_force_curr','payment_date') def _onchange_is_force_curr(self): self._set_currency_rate() @api.onchange('register_ids') def _onchange_register_ids(self): amount = 0.0 for line in self.register_ids: amount += line.amount_to_pay self.amount = amount return def get_payment_line_vals(self, payment, line): """ Hook for extension """ return { 'payment_id': payment.id, 'name': line.name, 'invoice_id': line.invoice_id.id, #'type': line.debit and 'dr' or 'cr', 'amount_total': line.amount_total, 'residual': line.residual, 'amount_to_pay': line.amount_to_pay, 'payment_difference': line.payment_difference, 'payment_difference_handling': line.payment_difference_handling, 'writeoff_account_id': line.writeoff_account_id and line.writeoff_account_id.id or False, } @api.multi def create_payment(self): payment = self.env['account.payment'].create(self.get_payment_vals()) if payment: for line in self._get_register_lines(self.register_ids): self.env['account.payment.line'].create(self.get_payment_line_vals(payment, line)) payment.write({'register_date': self.payment_date, 'is_force_curr': self.is_force_curr, 'force_rate': self.force_rate, 'payment_adm': self.payment_adm, 'card_number': self.card_number, 'card_type': self.card_type, }) payment.post_multi() return payment#{'type': 'ir.actions.act_window_close'}
class DonationDonation(models.Model): _name = 'donation.donation' _description = 'Donation' _order = 'id desc' _rec_name = 'display_name' _inherit = ['mail.thread'] @api.multi @api.depends('line_ids', 'line_ids.unit_price', 'line_ids.quantity', 'line_ids.product_id', 'donation_date', 'currency_id', 'company_id') def _compute_total(self): for donation in self: total = tax_receipt_total = 0.0 company_currency = donation.company_currency_id donation_currency = donation.currency_id # Do not consider other currencies for tax receipts # because, for the moment, only very very few countries # accept tax receipts from other countries, and never in another # currency. If you know such cases, please tell us and we will # update the code of this module for line in donation.line_ids: line_total = line.quantity * line.unit_price total += line_total if (donation_currency == company_currency and line.product_id.tax_receipt_ok): tax_receipt_total += line_total donation.amount_total = total donation_currency =\ donation.currency_id.with_context(date=donation.donation_date) total_company_currency = donation_currency.compute( total, donation.company_id.currency_id) donation.amount_total_company_currency = total_company_currency donation.tax_receipt_total = tax_receipt_total # We don't want a depends on partner_id.country_id, because if the partner # moves to another country, we want to keep the old country for # past donations to have good statistics @api.multi @api.depends('partner_id') def _compute_country_id(self): # Use sudo() to by-pass record rules, because the same partner # can have donations in several companies for donation in self: donation.sudo().country_id = donation.partner_id.country_id @api.model def _default_currency(self): company = self.env['res.company']._company_default_get( 'donation.donation') return company.currency_id @api.model def _get_default_requested_by(self): return self.env['res.users'].browse(self.env.uid) currency_id = fields.Many2one('res.currency', string='Currency', required=True, states={'done': [('readonly', True)]}, track_visibility='onchange', ondelete='restrict', default=_default_currency) partner_id = fields.Many2one('res.partner', string='Donor', required=True, index=True, states={'done': [('readonly', True)]}, track_visibility='onchange', ondelete='restrict') commercial_partner_id = fields.Many2one( related='partner_id.commercial_partner_id', string='Parent Donor', readonly=True, store=True, index=True) # country_id is here to have stats per country # WARNING : I can't put a related field, because when someone # writes on the country_id of a partner, it will trigger a write # on all it's donations, including donations in other companies # which will be blocked by the record rule country_id = fields.Many2one('res.country', string='Country', compute='_compute_country_id', store=True, readonly=True, copy=False) donation_by = fields.Many2one('res.users', 'Donation by', required=True, track_visibility='onchange', default=_get_default_requested_by) donation_collector = fields.Many2one('hr.employee', string='Collector', track_visibility='onchange') donation_place = fields.Many2one('donation.place', 'Donation Place', store=True) gov_id = fields.Many2one('govs.villages.gov', related='donation_by.gov_id', string='Gov', store=True) check_total = fields.Monetary(string='Check Amount', digits=dp.get_precision('Account'), states={'done': [('readonly', True)]}, currency_field='currency_id', track_visibility='onchange') amount_total = fields.Monetary(compute='_compute_total', string='Amount Total', currency_field='currency_id', store=True, digits=dp.get_precision('Account'), readonly=True) amount_total_company_currency = fields.Monetary( compute='_compute_total', string='Amount Total in Company Currency', currency_field='company_currency_id', store=True, digits=dp.get_precision('Account'), readonly=True) donation_date = fields.Date(string='Donation Date', required=True, default=fields.Date.context_today, states={'done': [('readonly', True)]}, index=True, track_visibility='onchange') company_id = fields.Many2one('res.company', string='Company', required=True, states={'done': [('readonly', True)]}, default=lambda self: self.env['res.company']. _company_default_get('donation.donation')) line_ids = fields.One2many('donation.line', 'donation_id', string='Donation Lines', states={'done': [('readonly', True)]}, copy=True) move_id = fields.Many2one('account.move', string='Account Move', readonly=True, copy=False) move_analytic_id = fields.Many2one('account.analytic.line', string='Account Analytic', readonly=True, copy=False) number = fields.Char(related='move_id.name', readonly=True, size=64, store=True, string='Donation Number') journal_id = fields.Many2one( 'account.journal', string='Payment Method', required=True, domain=[('type', 'in', ('bank', 'cash')), ('allow_donation', '=', True)], states={'done': [('readonly', True)]}, track_visibility='onchange', default=lambda self: self.env.user.context_donation_journal_id) payment_ref = fields.Char(string='Payment Reference', size=32, states={'done': [('readonly', True)]}) state = fields.Selection([ ('draft', 'Draft'), ('transfer', 'Transfer'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='State', readonly=True, copy=False, default='draft', index=True, track_visibility='onchange') company_currency_id = fields.Many2one(related='company_id.currency_id', string="Company Currency", readonly=True) campaign_id = fields.Many2one( 'donation.campaign', string='Donation Campaign', track_visibility='onchange', ondelete='restrict', default=lambda self: self.env.user.context_donation_campaign_id) display_name = fields.Char(string='Display Name', compute='_compute_display_name', readonly=True) #display_tag = fields.Char(string ='Tag') tax_receipt_id = fields.Many2one('donation.tax.receipt', string='Tax Receipt', readonly=True, copy=False) tax_receipt_option = fields.Selection( [ ('none', 'None'), ('each', 'For Each Donation'), ('annual', 'Annual Tax Receipt'), ], string='Tax Receipt Option', states={'done': [('readonly', True)]}, index=True) tax_receipt_total = fields.Monetary( compute='_compute_total', string='Eligible Tax Receipt Sub-total', store=True, currency_field='company_currency_id') tags_id = fields.Many2many('account.analytic.tag', string='Tags', readonly=True) donation_method = fields.Many2one('donation.instrument', store=True, string='Donation Method') account_id = fields.Many2one('account.account', related='donation_place.account_id', store=True, string='Account', readonly=True) #analytic = fields.Integer(string="id",compute='get_analytic_account_id_2') #analytic_tag = fields.Integer(string="tegid",compute='get_analytic_account_id_2') @api.multi @api.constrains('donation_date') def _check_donation_date(self): for donation in self: if donation.donation_date > fields.Date.context_today(self): # TODO No error pop-up to user : Odoo 9 BUG ? raise ValidationError( _('The date of the donation of %s should be today ' 'or in the past, not in the future!') % donation.partner_id.name) @api.multi def _prepare_each_tax_receipt(self): self.ensure_one() vals = { 'company_id': self.company_id.id, 'currency_id': self.company_currency_id.id, 'donation_date': self.donation_date, 'amount': self.tax_receipt_total, 'type': 'each', 'partner_id': self.commercial_partner_id.id, } return vals @api.model def _prepare_move_line_name(self): name = _('Donation of %s') % self.partner_id.name return name @api.multi def _prepare_counterpart_move_line(self, name, amount_total_company_cur, total_amount_currency, currency_id): self.ensure_one() precision = self.env['decimal.precision'].precision_get('Account') if float_compare(amount_total_company_cur, 0, precision_digits=precision) == 1: debit = amount_total_company_cur credit = 0 total_amount_currency = self.amount_total else: credit = amount_total_company_cur * -1 debit = 0 total_amount_currency = self.amount_total * -1 vals = { 'debit': debit, 'credit': credit, 'name': name, 'account_id': self.journal_id.default_debit_account_id.id, 'partner_id': self.commercial_partner_id.id, 'currency_id': currency_id, 'amount_currency': (currency_id and total_amount_currency or 0.0), } return vals @api.multi def _prepare_donation_move(self): self.ensure_one() if not self.journal_id.default_debit_account_id: raise UserError( _("Missing Default Debit Account on journal '%s'.") % self.journal_id.name) movelines = [] if self.company_id.currency_id.id != self.currency_id.id: currency_id = self.currency_id.id else: currency_id = False # Note : we can have negative donations for donors that use direct # debit when their direct debit rejected by the bank amount_total_company_cur = 0.0 total_amount_currency = 0.0 name = self._prepare_move_line_name() aml = {} # key = (account_id, analytic_account_id) # value = {'credit': ..., 'debit': ..., 'amount_currency': ...} precision = self.env['decimal.precision'].precision_get('Account') for donation_line in self.line_ids: if donation_line.in_kind: continue amount_total_company_cur += donation_line.amount_company_currency #account_id = donation_line.product_id.property_account_income_id.id #if not account_id: # account_id = donation_line.product_id.categ_id.\ # property_account_income_categ_id.id account_id = donation_line.get_account_id() if not account_id: raise UserError( _("Missing income account on product '%s' or on it's " "related product category") % donation_line.product_id.name) analytic_account_id = donation_line.get_analytic_account_id() amount_currency = 0.0 if float_compare(donation_line.amount_company_currency, 0, precision_digits=precision) == 1: credit = donation_line.amount_company_currency debit = 0 amount_currency = donation_line.amount * -1 else: debit = donation_line.amount_company_currency * -1 credit = 0 amount_currency = donation_line.amount # TODO Take into account the option group_invoice_lines ? if (account_id, analytic_account_id) in aml: aml[(account_id, analytic_account_id)]['credit'] += credit aml[(account_id, analytic_account_id)]['debit'] += debit aml[(account_id, analytic_account_id)]['amount_currency'] \ += amount_currency else: aml[(account_id, analytic_account_id)] = { 'credit': credit, 'debit': debit, 'amount_currency': amount_currency, } if not aml: # for full in-kind donation return False for (account_id, analytic_account_id), content in aml.iteritems(): movelines.append((0, 0, { 'name': name, 'credit': content['credit'], 'debit': content['debit'], 'account_id': account_id, 'analytic_account_id': analytic_account_id, 'partner_id': self.commercial_partner_id.id, 'currency_id': currency_id, 'amount_currency': (currency_id and content['amount_currency'] or 0.0), })) # counter-part ml_vals = self._prepare_counterpart_move_line( name, amount_total_company_cur, total_amount_currency, currency_id) movelines.append((0, 0, ml_vals)) vals = { 'journal_id': self.journal_id.id, 'date': self.donation_date, 'ref': self.payment_ref, 'line_ids': movelines, } return vals @api.one def _prepare_analytic_line(self): """ Prepare the values used to create() an account.analytic.line upon validation of an account.move.line having an analytic account. This method is intended to be extended in other modules. """ #for donation_line in self.line_ids: #if donation_line.in_kind: #continue #amount = (self.credit or 0.0) - (self.debit or 0.0) #account_id = self.line_ids.product_id.property_account_income_id.id #if not account_id: # account_id = self.line_ids.product_id.categ_id.property_account_income_categ_id.id name = self._prepare_move_line_name() vals = { 'name': name, 'date': self.donation_date, 'account_id': self.campaign_id.analytic_account_id.id, 'partner_id': self.commercial_partner_id.id, #'unit_amount': self.quantity, #'product_id': self.product_id and self.product_id.id or False, #'product_uom_id': self.product_uom_id and self.product_uom_id.id or False, 'unit_amount': False, 'product_id': False, 'product_uom_id': False, #'amount': self.company_currency_id.with_context(date=self.date or fields.Date.context_today(self)).compute(amount, self.analytic_account_id.currency_id) if self.analytic_account_id.currency_id else amount, 'amount': self.amount_total, 'general_account_id': self.donation_place.account_id.id, 'ref': self.payment_ref, 'move_id': self.move_id.id, 'user_id': self._uid, 'tag_ids': self.tags_id, } return vals @api.multi def transfer(self): for rec in self: rec.state = 'transfer' #self.env.cr.execute("SELECT place.tag_id FROM donation_donation inner join donation_place as place on donation_donation.donation_place = place.id where donation_donation.id= '%s'" %(self.id)) #res = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_tag_donation_line_rel(donation_line_id, account_analytic_tag_id) VALUES ('%s','%s')" %(rec.line_ids.id,res)) #self.env.cr.execute("SELECT gov.tag_id FROM donation_donation inner join govs_villages_gov as gov on donation_donation.gov_id = gov.id where donation_donation.id= '%s'" %(self.id)) #res2 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_tag_donation_line_rel(donation_line_id, account_analytic_tag_id) VALUES ('%s','%s')" %(rec.line_ids.id,res2)) #self.env.cr.execute("SELECT method.tag_id FROM donation_line inner join donation_instrument as method on donation_line.donation_method = method.id where donation_line.id= '%s'" %(self.line_ids.id)) #res3 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_tag_donation_line_rel(donation_line_id, account_analytic_tag_id) VALUES ('%s','%s')" %(rec.line_ids.id,res3)) self.env.cr.execute( "SELECT place.tag_id FROM donation_donation inner join donation_place as place on donation_donation.donation_place = place.id where donation_donation.id= '%s'" % (self.id)) res11 = self.env.cr.fetchone()[0] rec.env.cr.execute( "insert INTO account_analytic_tag_donation_donation_rel(donation_donation_id, account_analytic_tag_id) VALUES ('%s','%s')" % (rec.id, res11)) self.env.cr.execute( "SELECT gov.tag_id FROM donation_donation inner join govs_villages_gov as gov on donation_donation.gov_id = gov.id where donation_donation.id= '%s'" % (self.id)) res12 = self.env.cr.fetchone()[0] rec.env.cr.execute( "insert INTO account_analytic_tag_donation_donation_rel(donation_donation_id, account_analytic_tag_id) VALUES ('%s','%s')" % (rec.id, res12)) self.env.cr.execute( "SELECT method.tag_id FROM donation_donation inner join donation_instrument as method on donation_donation.donation_method = method.id where donation_donation.id= '%s'" % (self.id)) res13 = self.env.cr.fetchone()[0] rec.env.cr.execute( "insert INTO account_analytic_tag_donation_donation_rel(donation_donation_id, account_analytic_tag_id) VALUES ('%s','%s')" % (rec.id, res13)) @api.multi def validate(self): check_total = self.env['res.users'].has_group( 'donation.group_donation_check_total') precision = self.env['decimal.precision'].precision_get('Account') for donation in self: if not donation.line_ids: raise UserError( _("Cannot validate the donation of %s because it doesn't " "have any lines!") % donation.partner_id.name) if float_is_zero(donation.amount_total, precision_digits=precision): raise UserError( _("Cannot validate the donation of %s because the " "total amount is 0 !") % donation.partner_id.name) if donation.state != 'transfer': raise UserError( _("Cannot validate the donation of %s because it is not " "in transfer state.") % donation.partner_id.name) if check_total and donation.check_total != donation.amount_total: raise UserError( _("The amount of the donation of %s (%s) is different " "from the sum of the donation lines (%s).") % (donation.partner_id.name, donation.check_total, donation.amount_total)) vals = {'state': 'done'} if donation.amount_total: move_vals = donation._prepare_donation_move() move_analytic_vals = donation._prepare_analytic_line()[0] # when we have a full in-kind donation: no account move if move_vals: move = self.env['account.move'].create(move_vals) #move.post() move_id2 = move.id vals['move_id'] = move.id move_analytic = self.env['account.analytic.line'].create( move_analytic_vals) move_analytic2 = move_analytic.id analytic = self.campaign_id.analytic_account_id.id vals['move_analytic_id'] = move_analytic.id self.env.cr.execute( "SELECT id FROM account_move_line where move_id = '%s' and analytic_account_id ='%d'" % (move.id, analytic)) res111 = self.env.cr.fetchone()[0] #ress = (move.id*2) - 1 self.env.cr.execute( "UPDATE account_analytic_line set move_id= '%s' where id= '%d'" % (res111, move_analytic.id)) resss = self.account_id.id self.env.cr.execute( "UPDATE account_analytic_line set general_account_id= '%s' where id= '%d'" % (resss, move_analytic.id)) ressss = self.commercial_partner_id.id self.env.cr.execute( "UPDATE account_analytic_line set partner_id= '%s' where id= '%d'" % (ressss, move_analytic.id)) ress2 = move_analytic.id self.env.cr.execute( "SELECT place.tag_id FROM donation_donation inner join donation_place as place on donation_donation.donation_place = place.id where donation_donation.id= '%s'" % (self.id)) res11 = self.env.cr.fetchone()[0] self.env.cr.execute( "insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" % (ress2, res11)) self.env.cr.execute( "SELECT gov.tag_id FROM donation_donation inner join govs_villages_gov as gov on donation_donation.gov_id = gov.id where donation_donation.id= '%s'" % (self.id)) res12 = self.env.cr.fetchone()[0] self.env.cr.execute( "insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" % (ress2, res12)) self.env.cr.execute( "SELECT method.tag_id FROM donation_donation inner join donation_instrument as method on donation_donation.donation_method = method.id where donation_donation.id= '%s'" % (self.id)) res13 = self.env.cr.fetchone()[0] self.env.cr.execute( "insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" % (ress2, res13)) else: donation.message_post( _('Full in-kind donation: no account move generated')) if (donation.tax_receipt_option == 'each' and donation.tax_receipt_total and not donation.tax_receipt_id): receipt_vals = donation._prepare_each_tax_receipt() receipt = self.env['donation.tax.receipt'].create(receipt_vals) vals['tax_receipt_id'] = receipt.id donation.write(vals) return @api.multi def save_default_values(self): self.ensure_one() self.env.user.write({ 'context_donation_journal_id': self.journal_id.id, 'context_donation_campaign_id': self.campaign_id.id, }) #@api.multi #def analytic(self): # for rec in self: # ress = (self.move_id.id *2) - 1 # self.env.cr.execute("UPDATE account_analytic_line set move_id= '%s' where id= '%d'" %(ress,self.move_analytic_id.id)) # ress2 = self.move_analytic_id.id # self.env.cr.execute("SELECT place.tag_id FROM donation_donation inner join donation_place as place on donation_donation.donation_place = place.id where donation_donation.id= '%s'" %(self.id)) # res11 = self.env.cr.fetchone()[0] # rec.env.cr.execute("insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" %(ress2,res11)) # self.env.cr.execute("SELECT gov.tag_id FROM donation_donation inner join govs_villages_gov as gov on donation_donation.gov_id = gov.id where donation_donation.id= '%s'" %(self.id)) #res12 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" %(ress2,res12)) #self.env.cr.execute("SELECT method.tag_id FROM donation_donation inner join donation_instrument as method on donation_donation.donation_method = method.id where donation_donation.id= '%s'" %(self.id)) #res13 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" %(ress2,res13)) #self.env.cr.execute("SELECT place.tag_id FROM donation_donation inner join donation_place as place on donation_donation.donation_place = place.id where donation_donation.id= '%s'" %(self.id)) #res11 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" %(ress,res11)) #self.env.cr.execute("SELECT gov.tag_id FROM donation_donation inner join govs_villages_gov as gov on donation_donation.gov_id = gov.id where donation_donation.id= '%s'" %(self.id)) #res12 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" %(ress,res12)) #self.env.cr.execute("SELECT method.tag_id FROM donation_donation inner join donation_instrument as method on donation_donation.donation_method = method.id where donation_donation.id= '%s'" %(self.id)) #res13 = self.env.cr.fetchone()[0] #rec.env.cr.execute("insert INTO account_analytic_line_tag_rel(line_id, tag_id) VALUES ('%s','%s')" %(ress,res13)) @api.multi def done2cancel(self): '''from Done state to Cancel state''' for donation in self: if donation.tax_receipt_id: raise UserError( _("You cannot cancel this donation because " "it is linked to the tax receipt %s. You should first " "delete this tax receipt (but it may not be legally " "allowed).") % donation.tax_receipt_id.number) if donation.move_id: donation.move_id.button_cancel() donation.move_id.unlink() donation.state = 'cancel' @api.multi def cancel2draft(self): '''from Cancel state to Draft state''' for donation in self: if donation.move_id: raise UserError( _("A cancelled donation should not be linked to " "an account move")) if donation.tax_receipt_id: raise UserError( _("A cancelled donation should not be linked to " "a tax receipt")) donation.state = 'draft' @api.multi def unlink(self): for donation in self: if donation.state == 'done': raise UserError( _("The donation '%s' is in Done state, so you cannot " "delete it.") % donation.display_name) if donation.move_id: raise UserError( _("The donation '%s' is linked to an account move, " "so you cannot delete it.") % donation.display_name) if donation.tax_receipt_id: raise UserError( _("The donation '%s' is linked to the tax receipt %s, " "so you cannot delete it.") % (donation.display_name, donation.tax_receipt_id.number)) return super(DonationDonation, self).unlink() @api.multi @api.depends('state', 'partner_id', 'move_id') def _compute_display_name(self): for donation in self: if donation.state == 'draft': name = _('Draft Donation of %s') % donation.partner_id.name elif donation.state == 'cancel': name = _('Cancelled Donation of %s') % donation.partner_id.name else: name = donation.number donation.display_name = name @api.onchange('partner_id') def partner_id_change(self): if self.partner_id: self.tax_receipt_option = self.partner_id.tax_receipt_option @api.onchange('tax_receipt_option') def tax_receipt_option_change(self): res = {} if (self.partner_id and self.partner_id.tax_receipt_option == 'annual' and self.tax_receipt_option != 'annual'): res = { 'warning': { 'title': _('Error:'), 'message': _('You cannot change the Tax Receipt ' 'Option when it is Annual.'), }, } self.tax_receipt_option = 'annual' return res
class AccountVoucher(models.Model): @api.one @api.depends('move_id.line_ids.reconciled', 'move_id.line_ids.account_id.internal_type') def _check_paid(self): self.paid = any([((line.account_id.internal_type, 'in', ('receivable', 'payable')) and line.reconciled) for line in self.move_id.line_ids]) @api.model def _get_currency(self): journal = self.env['account.journal'].browse(self._context.get('journal_id', False)) if journal.currency_id: return journal.currency_id.id return self.env.user.company_id.currency_id.id @api.multi @api.depends('name', 'number') def name_get(self): return [(r.id, (r.number or _('Voucher'))) for r in self] @api.one @api.depends('journal_id', 'company_id') def _get_journal_currency(self): self.currency_id = self.journal_id.currency_id.id or self.company_id.currency_id.id @api.model def _default_journal(self): voucher_type = self._context.get('voucher_type', 'sale') company_id = self._context.get('company_id', self.env.user.company_id.id) domain = [ ('type', '=', voucher_type), ('company_id', '=', company_id), ] return self.env['account.journal'].search(domain, limit=1) @api.multi @api.depends('tax_correction', 'line_ids.price_subtotal') def _compute_total(self): for voucher in self: total = 0 tax_amount = 0 for line in voucher.line_ids: tax_info = line.tax_ids.compute_all(line.price_unit, self.currency_id, line.quantity, line.product_id, voucher.partner_id) total += tax_info.get('total_included', 0.0) tax_amount += sum([t.get('amount',0.0) for t in tax_info.get('taxes', False)]) voucher.amount = total + voucher.tax_correction voucher.tax_amount = tax_amount _name = 'account.voucher' _description = 'Accounting Voucher' _inherit = ['mail.thread'] _order = "date desc, id desc" voucher_type = fields.Selection([('sale', 'Sale'), ('purchase', 'Purchase')], string='Type', readonly=True, states={'draft': [('readonly', False)]}, oldname="type") name = fields.Char('Payment Reference', readonly=True, states={'draft': [('readonly', False)]}, default='') date = fields.Date(readonly=True, select=True, states={'draft': [('readonly', False)]}, help="Effective date for accounting entries", copy=False, default=fields.Date.context_today) journal_id = fields.Many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=_default_journal) account_id = fields.Many2one('account.account', 'Account', required=True, readonly=True, states={'draft': [('readonly', False)]}, domain=[('deprecated', '=', False)]) line_ids = fields.One2many('account.voucher.line', 'voucher_id', 'Voucher Lines', readonly=True, copy=True, states={'draft': [('readonly', False)]}) narration = fields.Text('Notes', readonly=True, states={'draft': [('readonly', False)]}) currency_id = fields.Many2one('res.currency', compute='_get_journal_currency', string='Currency', readonly=True, required=True, default=lambda self: self._get_currency()) company_id = fields.Many2one('res.company', 'Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['res.company']._company_default_get('account.voucher')) state = fields.Selection( [('draft', 'Draft'), ('cancel', 'Cancelled'), ('proforma', 'Pro-forma'), ('posted', 'Posted') ], 'Status', readonly=True, track_visibility='onchange', copy=False, default='draft', help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed Voucher. \ \n* The \'Pro-forma\' when voucher is in Pro-forma status,voucher does not have an voucher number. \ \n* The \'Posted\' status is used when user create voucher,a voucher number is generated and voucher entries are created in account \ \n* The \'Cancelled\' status is used when user cancel voucher.') reference = fields.Char('Bill Reference', readonly=True, states={'draft': [('readonly', False)]}, help="The partner reference of this document.", copy=False) amount = fields.Monetary(string='Total', store=True, readonly=True, compute='_compute_total') tax_amount = fields.Monetary(readonly=True, store=True, compute='_compute_total') tax_correction = fields.Monetary(readonly=True, states={'draft': [('readonly', False)]}, help='In case we have a rounding problem in the tax, use this field to correct it') number = fields.Char(readonly=True, copy=False) move_id = fields.Many2one('account.move', 'Journal Entry', copy=False) partner_id = fields.Many2one('res.partner', 'Partner', change_default=1, readonly=True, states={'draft': [('readonly', False)]}) paid = fields.Boolean(compute='_check_paid', help="The Voucher has been totally paid.") pay_now = fields.Selection([ ('pay_now', 'Pay Directly'), ('pay_later', 'Pay Later'), ], 'Payment', select=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_now') date_due = fields.Date('Due Date', readonly=True, select=True, states={'draft': [('readonly', False)]}) audit = fields.Boolean('To Review', related="move_id.to_check", help='Check this box if you are unsure of that journal entry and if you want to note it as \'to be reviewed\' by an accounting expert.') @api.onchange('partner_id') def onchange_partner_id(self): if self.journal_id.type == 'sale': account_id = self.partner_id.property_account_receivable_id.id elif self.journal_id.type == 'purchase': account_id = self.partner_id.property_account_payable_id.id elif self.voucher_type == 'sale': account_id = sef.journal_id.default_debit_account_id.id elif self.voucher_type == 'purchase': account_id = self.journal_id.default_credit_account_id.id else: account_id = self.journal_id.default_credit_account_id.id or self.journal_id.default_debit_account_id.id self.account_id = account_id @api.multi def button_proforma_voucher(self): self.signal_workflow('proforma_voucher') return {'type': 'ir.actions.act_window_close'} @api.multi def proforma_voucher(self): self.action_move_line_create() @api.multi def action_cancel_draft(self): self.create_workflow() self.write({'state': 'draft'}) @api.multi def cancel_voucher(self): for voucher in self: voucher.move_id.button_cancel() voucher.move_id.unlink() self.write({'state': 'cancel', 'move_id': False}) @api.multi def unlink(self): for voucher in self: if voucher.state not in ('draft', 'cancel'): raise Warning(_('Cannot delete voucher(s) which are already opened or paid.')) return super(AccountVoucher, self).unlink() @api.onchange('pay_now') def onchange_payment(self): account_id = False if self.pay_now == 'pay_later': partner = self.partner_id journal = self.journal_id if journal.type == 'sale': account_id = partner.property_account_receivable_id.id elif journal.type == 'purchase': account_id = partner.property_account_payable_id.id elif self.voucher_type == 'sale': account_id = journal.default_debit_account_id.id elif self.voucher_type == 'purchase': account_id = journal.default_credit_account_id.id else: account_id = journal.default_credit_account_id.id or journal.default_debit_account_id.id self.account_id = account_id @api.multi def first_move_line_get(self, move_id, company_currency, current_currency): debit = credit = 0.0 if self.voucher_type == 'purchase': credit = self._convert_amount(self.amount) elif self.voucher_type == 'sale': debit = self._convert_amount(self.amount) if debit < 0.0: debit = 0.0 if credit < 0.0: credit = 0.0 sign = debit - credit < 0 and -1 or 1 #set the first line of the voucher move_line = { 'name': self.name or '/', 'debit': debit, 'credit': credit, 'account_id': self.account_id.id, 'move_id': move_id, 'journal_id': self.journal_id.id, 'partner_id': self.partner_id.id, 'currency_id': company_currency != current_currency and current_currency or False, 'amount_currency': (sign * abs(self.amount) # amount < 0 for refunds if company_currency != current_currency else 0.0), 'date': self.date, 'date_maturity': self.date_due } return move_line @api.multi def account_move_get(self): if self.number: name = self.number elif self.journal_id.sequence_id: if not self.journal_id.sequence_id.active: raise UserError(_('Please activate the sequence of selected journal !')) name = self.journal_id.sequence_id.with_context(ir_sequence_date=self.date).next_by_id() else: raise UserError(_('Please define a sequence on the journal.')) move = { 'name': name, 'journal_id': self.journal_id.id, 'narration': self.narration, 'date': self.date, 'ref': self.reference, } return move @api.multi def _convert_amount(self, amount): ''' This function convert the amount given in company currency. It takes either the rate in the voucher (if the payment_rate_currency_id is relevant) either the rate encoded in the system. :param amount: float. The amount to convert :param voucher: id of the voucher on which we want the conversion :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date field in order to select the good rate to use. :return: the amount in the currency of the voucher's company :rtype: float ''' for voucher in self: return voucher.currency_id.compute(amount, voucher.company_id.currency_id) @api.multi def voucher_move_line_create(self, line_total, move_id, company_currency, current_currency): ''' Create one account move line, on the given account move, per voucher line where amount is not 0.0. It returns Tuple with tot_line what is total of difference between debit and credit and a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists). :param voucher_id: Voucher id what we are working with :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines. :param move_id: Account move wher those lines will be joined. :param company_currency: id of currency of the company to which the voucher belong :param current_currency: id of currency of the voucher :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method) :rtype: tuple(float, list of int) ''' prec = self.company_id.currency_id.rounding for line in self.line_ids: #create one move line per voucher line where amount is not 0.0 # AND (second part of the clause) only if the original move line was not having debit = credit = 0 (which is a legal value) if not line.price_subtotal and not (line.move_line_id and not float_compare(line.move_line_id.debit, line.move_line_id.credit, precision_digits=prec) and not float_compare(line.move_line_id.debit, 0.0, precision_digits=prec)): continue # convert the amount set on the voucher line into the currency of the voucher's company # this calls res_curreny.compute() with the right context, so that it will take either the rate on the voucher if it is relevant or will use the default behaviour amount = self._convert_amount(line.price_unit*line.quantity) move_line = { 'journal_id': self.journal_id.id, 'name': line.name or '/', 'account_id': line.account_id.id, 'move_id': move_id, 'partner_id': self.partner_id.id, 'analytic_account_id': line.account_analytic_id and line.account_analytic_id.id or False, 'quantity': 1, 'credit': abs(amount) if self.voucher_type == 'sale' else 0.0, 'debit': abs(amount) if self.voucher_type == 'purchase' else 0.0, 'date': self.date, 'tax_ids': [(4,t.id) for t in line.tax_ids], 'amount_currency': line.price_subtotal if current_currency != company_currency else 0.0, } self.env['account.move.line'].create(move_line) return line_total @api.multi def action_move_line_create(self): ''' Confirm the vouchers given in ids and create the journal entries for each of them ''' for voucher in self: local_context = dict(self._context, force_company=voucher.journal_id.company_id.id) if voucher.move_id: continue company_currency = voucher.journal_id.company_id.currency_id.id current_currency = voucher.currency_id.id or company_currency # we select the context to use accordingly if it's a multicurrency case or not # But for the operations made by _convert_amount, we always need to give the date in the context ctx = local_context.copy() ctx['date'] = voucher.date ctx['check_move_validity'] = False # Create the account move record. move = self.env['account.move'].create(voucher.account_move_get()) # Get the name of the account_move just created # Create the first line of the voucher move_line = self.env['account.move.line'].with_context(ctx).create(voucher.first_move_line_get(move.id, company_currency, current_currency)) line_total = move_line.debit - move_line.credit if voucher.voucher_type == 'sale': line_total = line_total - voucher._convert_amount(voucher.tax_amount) elif voucher.voucher_type == 'purchase': line_total = line_total + voucher._convert_amount(voucher.tax_amount) # Create one move line per voucher line where amount is not 0.0 line_total = voucher.with_context(ctx).voucher_move_line_create(line_total, move.id, company_currency, current_currency) # Add tax correction to move line if any tax correction specified if voucher.tax_correction != 0.0: tax_move_line = self.env['account.move.line'].search([('move_id', '=', move.id), ('tax_line_id', '!=', False)], limit=1) if len(tax_move_line): tax_move_line.write({'debit': tax_move_line.debit + voucher.tax_correction if tax_move_line.debit > 0 else 0, 'credit': tax_move_line.credit + voucher.tax_correction if tax_move_line.credit > 0 else 0}) # We post the voucher. voucher.write({ 'move_id': move.id, 'state': 'posted', 'number': move.name }) move.post() return True @api.multi def _track_subtype(self, init_values): if 'state' in init_values: return 'account_voucher.mt_voucher_state_change' return super(AccountVoucher, self)._track_subtype(init_values)
class AccountInvoice(models.Model): _inherit = 'account.invoice' # no gravado en iva # cc_vat_untaxed = fields.Monetary( cc_vat_untaxed_base_amount = fields.Monetary( compute="_get_currency_values", string='Company Cur. VAT Untaxed', ) # company currency default odoo fields cc_amount_total = fields.Monetary( compute="_get_currency_values", string='Company Cur. Total', ) cc_amount_untaxed = fields.Monetary( compute="_get_currency_values", string='Company Cur. Untaxed', ) cc_amount_tax = fields.Monetary( compute="_get_currency_values", string='Company Cur. Tax', ) # von iva cc_vat_amount = fields.Monetary( compute="_get_currency_values", string='Company Cur. VAT Amount', ) cc_other_taxes_amount = fields.Monetary( compute="_get_currency_values", string='Company Cur. Other Taxes Amount') @api.one @api.depends('currency_id') def _get_currency_values(self): # TODO si traer el rate de esta manera no resulta (por ej. porque # borran una linea de rate), entonces podemos hacerlo desde el move # mas o menos como hace account_invoice_currency o viendo el total de # debito o credito de ese mismo currency = self.currency_id.with_context( date=self.date_invoice or fields.Date.context_today(self)) if not currency: return False if self.company_id.currency_id == currency: self.cc_amount_untaxed = self.amount_untaxed self.cc_amount_tax = self.amount_tax self.cc_amount_total = self.amount_total self.cc_vat_untaxed_base_amount = self.vat_untaxed_base_amount self.cc_vat_amount = self.vat_amount self.cc_other_taxes_amount = self.other_taxes_amount # self.currency_rate = 1.0 else: currency_rate = currency.compute(1.0, self.company_id.currency_id, round=False) # otra alternativa serua usar currency.compute con round true # para cada uno de estos valores # self.currency_rate = currency_rate self.cc_amount_untaxed = currency.round(self.amount_untaxed * currency_rate) self.cc_amount_tax = currency.round(self.amount_tax * currency_rate) self.cc_amount_total = currency.round(self.amount_total * currency_rate) self.cc_vat_untaxed_base_amount = currency.round( self.vat_untaxed_base_amount * currency_rate) self.cc_vat_amount = currency.round(self.vat_amount * currency_rate) self.cc_other_taxes_amount = currency.round( self.other_taxes_amount * currency_rate)
class account_voucher_line(models.Model): _name = 'account.voucher.line' _description = 'Voucher Lines' @api.one @api.depends('price_unit', 'tax_ids', 'quantity', 'product_id', 'voucher_id.currency_id') def _compute_subtotal(self): self.price_subtotal = self.quantity * self.price_unit if self.tax_ids: taxes = self.tax_ids.compute_all(self.price_unit, self.voucher_id.currency_id, self.quantity, product=self.product_id, partner=self.voucher_id.partner_id) self.price_subtotal = taxes['total_excluded'] name = fields.Text(string='Description', required=True) sequence = fields.Integer(default=10, help="Gives the sequence of this line when displaying the voucher.") voucher_id = fields.Many2one('account.voucher', 'Voucher', required=1, ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', ondelete='set null', index=True) account_id = fields.Many2one('account.account', string='Account', required=True, domain=[('deprecated', '=', False)], help="The income or expense account related to the selected product.") price_unit = fields.Monetary(string='Unit Price', required=True) price_subtotal = fields.Monetary(string='Amount', store=True, readonly=True, compute='_compute_subtotal') quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'), required=True, default=1) account_id = fields.Many2one('account.account', 'Account', required=True, domain=[('deprecated', '=', False)]) account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account') company_id = fields.Many2one('res.company', related='voucher_id.company_id', string='Company', store=True, readonly=True) tax_ids = fields.Many2many('account.tax', string='Tax', help="Only for tax excluded from price") currency_id = fields.Many2one('res.currency', related='voucher_id.currency_id') def _get_account(self, product, fpos, type): accounts = product.product_tmpl_id.get_product_accounts(fpos) if type == 'sale': return accounts['income'] return accounts['expense'] @api.multi def product_id_change(self, product_id, partner_id=False, price_unit=False, company_id=None, currency_id=None, type=None): context = self._context company_id = company_id if company_id is not None else context.get('company_id', False) company = self.env['res.company'].browse(company_id) currency = self.env['res.currency'].browse(currency_id) if not partner_id: raise UserError(_("You must first select a partner!")) part = self.env['res.partner'].browse(partner_id) if part.lang: self = self.with_context(lang=part.lang) product = self.env['product.product'].browse(product_id) fpos = part.property_account_position_id.id account = self._get_account(product, fpos, type) values = { 'name': product.partner_ref, 'account_id': account.id, } if type == 'purchase': values['price_unit'] = price_unit or product.standard_price taxes = product.supplier_taxes_id or account.tax_ids if product.description_purchase: values['name'] += '\n' + product.description_purchase else: values['price_unit'] = product.lst_price taxes = product.taxes_id or account.tax_ids if product.description_sale: values['name'] += '\n' + product.description_sale values['tax_ids'] = taxes.ids if company and currency: if company.currency_id != currency: if type == 'purchase': values['price_unit'] = product.standard_price values['price_unit'] = values['price_unit'] * currency.rate return {'value': values, 'domain': {}}
class AccountMoveLine(models.Model): _inherit = 'account.move.line' statement_line_id = fields.Many2one('account.bank.statement.line', related='move_id.statement_line_id', string='Bank Statement Line') other_balance = fields.Monetary(string='Other Balance', default=0.0) @api.model def create(self, vals): debit = vals.get('debit', 0) credit = vals.get('credit', 0) vals.update({'other_balance': credit - debit}) return super(AccountMoveLine, self).create(vals) @api.multi def write(self, vals): self.calculate_orther_balance(vals) return super(AccountMoveLine, self).write(vals) @api.multi def calculate_orther_balance(self, vals): for record in self: if 'debit' in vals or 'credit' in vals: debit = vals.get('debit', record.debit) credit = vals.get('credit', record.credit) vals.update({ 'other_balance': credit - debit, }) return True @api.multi @api.constrains('move_id', 'account_id') def check_account_type_bank(self): for line in self: if line.move_id: line_account_bank = line.move_id.line_ids.filtered( lambda a: a.account_id.reconciled_account) if len(line_account_bank) > 1: raise UserError( _('Only one journal item on an account requiring ' + 'bank reconciliation can be booked in this ' + 'account move. It is impossible to add another ' + 'one. Please create a distinct account move to ' + 'registrer this account.move.line and its ' + 'counterpart.')) def _create_writeoff(self, vals): res = super(AccountMoveLine, self)._create_writeoff(vals) partner = self.mapped('partner_id') lines = res.mapped('move_id.line_ids') for line in lines: line.partner_id = partner.id if len(partner) == 1 and\ not any(not line.partner_id for line in self)\ else False return res @api.multi def unmatch_bankstatement_wizard(self): active_ids = self._context.get('active_ids', []) active_model = self._context.get('active_model', []) view_id = self.env.ref( 'coop_account.view_unmatch_bank_statement_wizard_form') mess_confirm = _('Are you sure you want to unmatch %s transactions?') %\ (len(active_ids)) wizard = self.env['unmatch.bank.statement.wizard'].create( {'mess_confirm': mess_confirm}) return { 'name': _('Unmatch Bank Statement'), 'type': 'ir.actions.act_window', 'view_id': view_id.id, 'view_mode': 'form', 'view_type': 'form', 'res_id': wizard.id, 'res_model': 'unmatch.bank.statement.wizard', 'target': 'new', 'context': { 'active_ids': active_ids, 'active_model': active_model } } @api.model def export_wrong_reconciliation_ml(self): wrong_reconciliation_ml_data = self.get_wrong_reconciliation_ml_data() data = { 'model': self._name, 'headers': wrong_reconciliation_ml_data['headers'], 'rows': wrong_reconciliation_ml_data['rows'] } return json.dumps(data) @api.model def get_wrong_reconciliation_ml_data(self): read_columns = [ 'name', 'journal_id', 'ref', 'date', 'partner_id', 'company_id', 'check_holder_name', 'account_id', 'move_id', 'debit', 'credit', 'quantity', 'statement_line_id', 'statement_id', 'payment_id', 'check_deposit_id' ] model_fields = self._fields header_columns = [ getattr(model_fields[column], 'string') for column in read_columns ] header_columns = ['External ID'] + header_columns read_columns = ['id'] + read_columns wrong_move_lines = self.get_wrong_reconciliation_ml() if not wrong_move_lines: raise Warning(_('Found no wrong account move lines.')) record_values_dict = wrong_move_lines.export_data(read_columns) converted_data_rows = [[unicode(cell_data) for cell_data in data_row] for data_row in record_values_dict['datas']] return { 'rows': converted_data_rows, 'headers': header_columns, } @api.model def get_wrong_reconciliation_ml(self): wrong_move_lines = self selected_journals = self.env['account.journal'].search([ ('name', 'not like', '%Chèques%'), '|', ('name', 'like', 'CCOOP - compte courant'), ('name', 'ilike', '%cep%'), ]) bank_statement_line_query = """ select statement_line_id from account_move where statement_line_id is not null and journal_id in {} group by statement_line_id having count(statement_line_id) > 1 """.format(tuple(selected_journals.ids)) self.env.cr.execute(bank_statement_line_query) results = self.env.cr.fetchall() line_ids = [id_tuple[0] for id_tuple in results] bank_statement_line_ids = self.env['account.bank.statement.line']\ .browse(line_ids) for stml in bank_statement_line_ids: stml_date = fields.Date.from_string(stml.date) move_ids = stml.journal_entry_ids move_line_ids = move_ids.mapped('line_ids') if len(move_ids) == 1 and len(move_line_ids) == 2: continue for aml in move_line_ids.filtered(lambda ml: not ml.statement_id): aml_date = fields.Date.from_string(aml.date) if aml_date.year <= stml_date.year and \ aml_date.month <= stml_date.month: continue else: wrong_move_lines |= aml return wrong_move_lines @api.model def run_reconcile_411_pos(self, nb_lines_per_job=100): # Prepare session for job session = ConnectorSession(self._cr, self._uid, context=self.env.context) parameter_obj = self.env['ir.config_parameter'] reconcile_pos_account = parameter_obj.get_param( 'to.reconcile.pos.account', False) reconcile_pos_account = self.env['account.account'].search( [('code', '=', reconcile_pos_account)], limit=1) if not reconcile_pos_account: _logger.warn( "Couldn't find account with code %s," "please set value for key 'to.reconcile.pos.account'" " in config_parameter", reconcile_pos_account) return True debit_moves_domain = [ ('reconciled', '=', False), ('account_id', '=', reconcile_pos_account and reconcile_pos_account.id or False), ('partner_id', '!=', None), ('debit', '>', 0), ('credit', '=', 0) ] # Create jobs lines = self.search(debit_moves_domain, order='id') # Reconcile the journal items by partner partner_ids = list(set(lines.mapped('partner_id.id'))) total_lines = len(partner_ids) job_lines = nb_lines_per_job number_of_jobs = int(total_lines / job_lines) + \ (total_lines % job_lines > 0) start_line = 0 for i in range(1, number_of_jobs + 1): start_line = i * job_lines - job_lines end_line = i * job_lines chunk_ids = partner_ids[start_line:end_line] if i == number_of_jobs: chunk_ids = partner_ids[start_line:] session = ConnectorSession(self._cr, self._uid, context=self.env.context) job_reconcile_411_pos_by_ref.delay(session, 'res.partner', chunk_ids, reconcile_pos_account.id)
class account_payment(models.Model): _name = "account.payment" _inherit = 'account.abstract.payment' _description = "Payments" _order = "payment_date desc, name desc" @api.one @api.depends('invoice_ids') def _get_has_invoices(self): self.has_invoices = bool(self.invoice_ids) @api.one @api.depends('invoice_ids', 'amount', 'payment_date', 'currency_id') def _compute_payment_difference(self): if len(self.invoice_ids) == 0: return self.payment_difference = self._compute_total_invoices_amount() - self.amount company_id = fields.Many2one(store=True) name = fields.Char(readonly=True, copy=False, default="Draft Payment") # The name is attributed upon post() state = fields.Selection([('draft', 'Draft'), ('posted', 'Posted'), ('sent', 'Sent'), ('reconciled', 'Reconciled')], readonly=True, default='draft', copy=False, string="Status") payment_type = fields.Selection(selection_add=[('transfer', 'Internal Transfer')]) payment_reference = fields.Char(copy=False, readonly=True, help="Reference of the document used to issue this payment. Eg. check number, file name, etc.") # Money flows from the journal_id's default_debit_account_id or default_credit_account_id to the destination_account_id destination_account_id = fields.Many2one('account.account', compute='_compute_destination_account_id', readonly=True) # For money transfer, money goes from journal_id to a transfer account, then from the transfer account to destination_journal_id destination_journal_id = fields.Many2one('account.journal', string='Transfer To', domain=[('type', 'in', ('bank', 'cash'))]) invoice_ids = fields.Many2many('account.invoice', 'account_invoice_payment_rel', 'payment_id', 'invoice_id', string="Invoices", copy=False, readonly=True) has_invoices = fields.Boolean(compute="_get_has_invoices", help="Technical field used for usablity purposes") payment_difference = fields.Monetary(compute='_compute_payment_difference', readonly=True) payment_difference_handling = fields.Selection([('open', 'Keep open'), ('reconcile', 'Mark invoice as fully paid')], default='open', string="Payment Difference", copy=False) writeoff_account_id = fields.Many2one('account.account', string="Difference Account", domain=[('deprecated', '=', False)], copy=False) # FIXME: ondelete='restrict' not working (eg. cancel a bank statement reconciliation with a payment) move_line_ids = fields.One2many('account.move.line', 'payment_id', readonly=True, copy=False, ondelete='restrict') @api.one @api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id') def _compute_destination_account_id(self): if self.invoice_ids: self.destination_account_id = self.invoice_ids[0].account_id.id elif self.payment_type == 'transfer': if not self.company_id.transfer_account_id.id: raise UserError(_('Transfer account not defined on the company.')) self.destination_account_id = self.company_id.transfer_account_id.id elif self.partner_id: if self.partner_type == 'customer': self.destination_account_id = self.partner_id.property_account_receivable_id.id else: self.destination_account_id = self.partner_id.property_account_payable_id.id @api.onchange('partner_type') def _onchange_partner_type(self): # Set partner_id domain if self.partner_type: return {'domain': {'partner_id': [(self.partner_type, '=', True)]}} @api.onchange('payment_type') def _onchange_payment_type(self): if not self.invoice_ids: # Set default partner type for the payment type if self.payment_type == 'inbound': self.partner_type = 'customer' elif self.payment_type == 'outbound': self.partner_type = 'supplier' # Set payment method domain res = self._onchange_journal() if not res.get('domain', {}): res['domain'] = {} res['domain']['journal_id'] = self.payment_type == 'inbound' and [('at_least_one_inbound', '=', True)] or [('at_least_one_outbound', '=', True)] res['domain']['journal_id'].append(('type', 'in', ('bank', 'cash'))) return res @api.model def default_get(self, fields): rec = super(account_payment, self).default_get(fields) invoice_defaults = self.resolve_2many_commands('invoice_ids', rec.get('invoice_ids')) if invoice_defaults and len(invoice_defaults) == 1: invoice = invoice_defaults[0] rec['communication'] = invoice['reference'] rec['currency_id'] = invoice['currency_id'][0] rec['payment_type'] = invoice['type'] in ('out_invoice', 'in_refund') and 'inbound' or 'outbound' rec['partner_type'] = MAP_INVOICE_TYPE_PARTNER_TYPE[invoice['type']] rec['partner_id'] = invoice['partner_id'][0] rec['amount'] = invoice['residual'] return rec def _get_invoices(self): return self.invoice_ids @api.multi def button_journal_entries(self): return { 'name': _('Journal Items'), 'view_type': 'form', 'view_mode': 'tree', 'res_model': 'account.move.line', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('payment_id', 'in', self.ids)], } @api.multi def button_invoices(self): return { 'name': _('Paid Invoices'), 'view_type': 'form', 'view_mode': 'tree', 'res_model': 'account.invoice', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('id', 'in', [x.id for x in self.invoice_ids])], } @api.multi def button_dummy(self): return True @api.multi def cancel(self): for rec in self: for move in rec.move_line_ids.mapped('move_id'): if rec.invoice_ids: move.line_ids.remove_move_reconcile() move.button_cancel() move.unlink() rec.state = 'draft' @api.multi def unlink(self): if any(rec.state != 'draft' for rec in self): raise UserError(_("You can not delete a payment that is already posted")) return super(account_payment, self).unlink() @api.multi def post(self): """ Create the journal items for the payment and update the payment's state to 'posted'. A journal entry is created containing an item in the source liquidity account (selected journal's default_debit or default_credit) and another in the destination reconciliable account (see _compute_destination_account_id). If invoice_ids is not empty, there will be one reconciliable move line per invoice to reconcile with. If the payment is a transfer, a second journal entry is created in the destination journal to receive money from the transfer account. """ for rec in self: if rec.state != 'draft': raise UserError(_("Only a draft payment can be posted. Trying to post a payment in state %s.") % rec.state) if any(inv.state != 'open' for inv in rec.invoice_ids): raise ValidationError(_("The payment cannot be processed because the invoice is not open!")) # Use the right sequence to set the name if rec.payment_type == 'transfer': sequence = rec.env.ref('account.sequence_payment_transfer') else: if rec.partner_type == 'customer': if rec.payment_type == 'inbound': sequence = rec.env.ref('account.sequence_payment_customer_invoice') if rec.payment_type == 'outbound': sequence = rec.env.ref('account.sequence_payment_customer_refund') if rec.partner_type == 'supplier': if rec.payment_type == 'inbound': sequence = rec.env.ref('account.sequence_payment_supplier_refund') if rec.payment_type == 'outbound': sequence = rec.env.ref('account.sequence_payment_supplier_invoice') rec.name = sequence.with_context(ir_sequence_date=rec.payment_date).next_by_id() # Create the journal entry amount = rec.amount * (rec.payment_type in ('outbound', 'transfer') and 1 or -1) move = rec._create_payment_entry(amount) # In case of a transfer, the first journal entry created debited the source liquidity account and credited # the transfer account. Now we debit the transfer account and credit the destination liquidity account. if rec.payment_type == 'transfer': transfer_credit_aml = move.line_ids.filtered(lambda r: r.account_id == rec.company_id.transfer_account_id) transfer_debit_aml = rec._create_transfer_entry(amount) (transfer_credit_aml + transfer_debit_aml).reconcile() rec.state = 'posted' def _create_payment_entry(self, amount): """ Create a journal entry corresponding to a payment, if the payment references invoice(s) they are reconciled. Return the journal entry. """ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) move = self.env['account.move'].create(self._get_move_vals()) #Write line corresponding to invoice payment counterpart_aml_dict = self._get_shared_move_line_vals(debit, credit, amount_currency, move.id, False) counterpart_aml_dict.update(self._get_counterpart_move_line_vals(self.invoice_ids)) counterpart_aml_dict.update({'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False}) counterpart_aml = aml_obj.create(counterpart_aml_dict) #Reconcile with the invoices if self.payment_difference_handling == 'reconcile': self.invoice_ids.register_payment(counterpart_aml, self.writeoff_account_id, self.journal_id) else: self.invoice_ids.register_payment(counterpart_aml) #Write counterpart lines liquidity_aml_dict = self._get_shared_move_line_vals(credit, debit, -amount_currency, move.id, False) liquidity_aml_dict.update(self._get_liquidity_move_line_vals(-amount)) aml_obj.create(liquidity_aml_dict) move.post() return move def _create_transfer_entry(self, amount): """ Create the journal entry corresponding to the 'incoming money' part of an internal transfer, return the reconciliable move line """ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False) debit, credit, amount_currency = aml_obj.with_context(date=self.payment_date).compute_amount_fields(amount, self.currency_id, self.company_id.currency_id) amount_currency = self.destination_journal_id.currency_id and self.currency_id.with_context(date=self.payment_date).compute(amount, self.destination_journal_id.currency_id) or 0 dst_move = self.env['account.move'].create(self._get_move_vals(self.destination_journal_id)) dst_liquidity_aml_dict = self._get_shared_move_line_vals(debit, credit, amount_currency, dst_move.id) dst_liquidity_aml_dict.update({ 'name': _('Transfer from %s') % self.journal_id.name, 'account_id': self.destination_journal_id.default_credit_account_id.id, 'currency_id': self.destination_journal_id.currency_id.id, 'payment_id': self.id, 'journal_id': self.destination_journal_id.id}) aml_obj.create(dst_liquidity_aml_dict) transfer_debit_aml_dict = self._get_shared_move_line_vals(credit, debit, 0, dst_move.id) transfer_debit_aml_dict.update({ 'name': self.name, 'payment_id': self.id, 'account_id': self.company_id.transfer_account_id.id, 'journal_id': self.destination_journal_id.id}) if self.currency_id != self.company_id.currency_id: transfer_debit_aml_dict.update({ 'currency_id': self.currency_id.id, 'amount_currency': -self.amount, }) transfer_debit_aml = aml_obj.create(transfer_debit_aml_dict) dst_move.post() return transfer_debit_aml def _get_move_vals(self, journal=None): """ Return dict to create the payment move """ journal = journal or self.journal_id if not journal.sequence_id: raise UserError(_('Configuration Error !'), _('The journal %s does not have a sequence, please specify one.') % journal.name) if not journal.sequence_id.active: raise UserError(_('Configuration Error !'), _('The sequence of journal %s is deactivated.') % journal.name) name = journal.with_context(ir_sequence_date=self.payment_date).sequence_id.next_by_id() return { 'name': name, 'date': self.payment_date, 'ref': self.communication or '', 'company_id': self.company_id.id, 'journal_id': journal.id, } def _get_shared_move_line_vals(self, debit, credit, amount_currency, move_id, invoice_id=False): """ Returns values common to both move lines (except for debit, credit and amount_currency which are reversed) """ return { 'partner_id': self.payment_type in ('inbound', 'outbound') and self.env['res.partner']._find_accounting_partner(self.partner_id).id or False, 'invoice_id': invoice_id and invoice_id.id or False, 'move_id': move_id, 'debit': debit, 'credit': credit, 'amount_currency': amount_currency or False, } def _get_counterpart_move_line_vals(self, invoice=False): if self.payment_type == 'transfer': name = self.name else: name = '' if self.partner_type == 'customer': if self.payment_type == 'inbound': name += _("Customer Payment") elif self.payment_type == 'outbound': name += _("Customer Refund") elif self.partner_type == 'supplier': if self.payment_type == 'inbound': name += _("Vendor Refund") elif self.payment_type == 'outbound': name += _("Vendor Payment") if invoice: name += ': ' for inv in invoice: name += inv.number+', ' name = name[:len(name)-2] return { 'name': name, 'account_id': self.destination_account_id.id, 'journal_id': self.journal_id.id, 'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False, 'payment_id': self.id, } def _get_liquidity_move_line_vals(self, amount): name = self.name if self.payment_type == 'transfer': name = _('Transfer to %s') % self.destination_journal_id.name vals = { 'name': name, 'account_id': self.payment_type in ('outbound','transfer') and self.journal_id.default_debit_account_id.id or self.journal_id.default_credit_account_id.id, 'payment_id': self.id, 'journal_id': self.journal_id.id, 'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False, } # If the journal has a currency specified, the journal item need to be expressed in this currency if self.journal_id.currency_id and self.currency_id != self.journal_id.currency_id: amount = self.currency_id.with_context(date=self.payment_date).compute(amount, self.journal_id.currency_id) debit, credit, amount_currency = self.env['account.move.line'].with_context(date=self.payment_date).compute_amount_fields(amount, self.journal_id.currency_id, self.company_id.currency_id) vals.update({ 'amount_currency': amount_currency, 'currency_id': self.journal_id.currency_id.id, }) return vals
class account_analytic_account(models.Model): _name = 'account.analytic.account' _inherit = ['mail.thread'] _description = 'Analytic Account' _order = 'code, name asc' @api.multi def _compute_debit_credit_balance(self): analytic_line_obj = self.env['account.analytic.line'] domain = [('account_id', 'in', self.mapped('id'))] if self._context.get('from_date', False): domain.append(('date', '>=', self._context['from_date'])) if self._context.get('to_date', False): domain.append(('date', '<=', self._context['to_date'])) account_amounts = analytic_line_obj.search_read( domain, ['account_id', 'amount']) account_ids = set([line['account_id'][0] for line in account_amounts]) data_debit = {account_id: 0.0 for account_id in account_ids} data_credit = {account_id: 0.0 for account_id in account_ids} for account_amount in account_amounts: if account_amount['amount'] < 0.0: data_debit[account_amount['account_id'] [0]] += account_amount['amount'] else: data_credit[account_amount['account_id'] [0]] += account_amount['amount'] for account in self: account.debit = abs(data_debit.get(account.id, 0.0)) account.credit = data_credit.get(account.id, 0.0) account.balance = account.credit - account.debit name = fields.Char(string='Analytic Account', index=True, required=True, track_visibility='onchange') code = fields.Char(string='Reference', index=True, track_visibility='onchange') account_type = fields.Selection([('normal', 'Analytic View')], string='Type of Account', required=True, default='normal') tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_account_tag_rel', 'account_id', 'tag_id', string='Tags', copy=True) line_ids = fields.One2many('account.analytic.line', 'account_id', string="Analytic Lines") company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) partner_id = fields.Many2one('res.partner', string='Customer') balance = fields.Monetary(compute='_compute_debit_credit_balance', string='Balance') debit = fields.Monetary(compute='_compute_debit_credit_balance', string='Debit') credit = fields.Monetary(compute='_compute_debit_credit_balance', string='Credit') currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True) @api.multi def name_get(self): res = [] for analytic in self: name = analytic.name if analytic.code: name = '[' + analytic.code + '] ' + name if analytic.partner_id: name = name + ' - ' + analytic.partner_id.commercial_partner_id.name res.append((analytic.id, name)) return res @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): args = args or [] recs = self.search([ '|', '|', ('code', operator, name), ('partner_id', operator, name), ('name', operator, name) ] + args, limit=limit) return recs.name_get() @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'open': return 'analytic.mt_account_opened' elif 'state' in init_values and self.state == 'close': return 'analytic.mt_account_closed' elif 'state' in init_values and self.state == 'pending': return 'analytic.mt_account_pending' return super(account_analytic_account, self)._track_subtype(init_values)
class account_abstract_payment(models.AbstractModel): _name = "account.abstract.payment" _description = "Contains the logic shared between models which allows to register payments" payment_type = fields.Selection([('outbound', 'Send Money'), ('inbound', 'Receive Money')], string='Payment Type', required=True) payment_method_id = fields.Many2one('account.payment.method', string='Payment Type', required=True, oldname="payment_method") payment_method_code = fields.Char(related='payment_method_id.code', help="Technical field used to adapt the interface to the payment type selected.") partner_type = fields.Selection([('customer', 'Customer'), ('supplier', 'Vendor')]) partner_id = fields.Many2one('res.partner', string='Partner') amount = fields.Monetary(string='Payment Amount', required=True) currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id) payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True, copy=False) communication = fields.Char(string='Memo') journal_id = fields.Many2one('account.journal', string='Payment Method', required=True, domain=[('type', 'in', ('bank', 'cash'))]) company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True) hide_payment_method = fields.Boolean(compute='_compute_hide_payment_method', help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'") @api.one @api.constrains('amount') def _check_amount(self): if not self.amount > 0.0: raise ValidationError('The payment amount must be strictly positive.') @api.one @api.depends('payment_type', 'journal_id') def _compute_hide_payment_method(self): if not self.journal_id: self.hide_payment_method = True return journal_payment_methods = self.payment_type == 'inbound' and self.journal_id.inbound_payment_method_ids or self.journal_id.outbound_payment_method_ids self.hide_payment_method = len(journal_payment_methods) == 1 and journal_payment_methods[0].code == 'manual' @api.onchange('journal_id') def _onchange_journal(self): if self.journal_id: self.currency_id = self.journal_id.currency_id or self.company_id.currency_id # Set default payment method (we consider the first to be the default one) payment_methods = self.payment_type == 'inbound' and self.journal_id.inbound_payment_method_ids or self.journal_id.outbound_payment_method_ids self.payment_method_id = payment_methods and payment_methods[0] or False # Set payment method domain (restrict to methods enabled for the journal and to selected payment type) payment_type = self.payment_type in ('outbound', 'transfer') and 'outbound' or 'inbound' return {'domain': {'payment_method_id': [('payment_type', '=', payment_type), ('id', 'in', payment_methods.ids)]}} return {} def _get_invoices(self): """ Return the invoices of the payment. Must be overridden """ raise NotImplementedError def _compute_total_invoices_amount(self): """ Compute the sum of the residual of invoices, expressed in the payment currency """ total = 0 payment_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id for inv in self._get_invoices(): total += inv.residual_company_signed if self.company_id and self.company_id.currency_id != payment_currency: total = self.company_id.currency_id.with_context(date=self.payment_date).compute(total, payment_currency) return abs(total)
class PurchaseSuggest(models.TransientModel): _name = 'purchase.suggest' _description = 'Purchase Suggestions' _rec_name = 'product_id' # campos y métodos agregados agregados replenishment_cost = fields.Float( readonly=True, ) currency_id = fields.Many2one( 'res.currency', readonly=True, ) virtual_available = fields.Float( string='Forecasted Quantity', digits=dp.get_precision('Product Unit of Measure'), readonly=True, help="Forecast quantity in the unit of measure of the product " "(computed as Quantity On Hand - Outgoing + Incoming + Draft PO " "quantity)" ) rotation = fields.Float( readonly=True, ) location_rotation = fields.Float( readonly=True, ) order_amount = fields.Monetary( string='Order Amount', compute='_compute_order_amount', store=True, ) qty_multiple = fields.Float( string="Qty Multiple", readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="in the unit of measure for the product" ) @api.multi @api.depends('qty_to_order', 'replenishment_cost') def _compute_order_amount(self): for rec in self: rec.order_amount = rec.replenishment_cost * rec.qty_to_order @api.multi def action_traceability(self): self.ensure_one() action = self.env.ref('stock.act_product_stock_move_open') if action: action_read = action.read()[0] # nos da error al querer leerlo como dict # context = literal_eval(action_read['context']) # context['search_default_product_id'] = self.product_id.id # context['default_product_id'] = self.product_id.id context = { 'search_default_future': 1, 'search_default_picking_type': 1, 'search_default_product_id': self.product_id.id, 'default_product_id': self.product_id.id} action_read['context'] = context return action_read # campos originales de purchase suggest company_id = fields.Many2one( 'res.company', string='Company', required=True) product_id = fields.Many2one( 'product.product', string='Product', required=True, readonly=True) uom_id = fields.Many2one( 'product.uom', string='UoM', related='product_id.uom_id', readonly=True) uom_po_id = fields.Many2one( 'product.uom', string='Purchase UoM', related='product_id.uom_po_id', readonly=True) seller_id = fields.Many2one( 'res.partner', string='Main Supplier', readonly=True, domain=[('supplier', '=', True)]) qty_available = fields.Float( string='Quantity On Hand', readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="in the unit of measure of the product") incoming_qty = fields.Float( string='Incoming Quantity', readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="in the unit of measure of the product") outgoing_qty = fields.Float( string='Outgoing Quantity', readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="in the unit of measure of the product") draft_po_qty = fields.Float( string='Draft PO Quantity', readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="Draft purchase order quantity in the unit of measure " "of the product (NOT in the purchase unit of measure !)") last_po_line_id = fields.Many2one( 'purchase.order.line', string='Last Purchase Order Line', readonly=True) last_po_date = fields.Datetime( related='last_po_line_id.order_id.date_order', string='Date of the Last Order', readonly=True) last_po_qty = fields.Float( related='last_po_line_id.product_qty', readonly=True, digits=dp.get_precision('Product Unit of Measure'), string='Quantity of the Last Order') last_po_uom = fields.Many2one( related='last_po_line_id.product_uom', readonly=True, string='UoM of the Last Order') orderpoint_id = fields.Many2one( 'stock.warehouse.orderpoint', string='Re-ordering Rule', readonly=True) location_id = fields.Many2one( 'stock.location', string='Stock Location', readonly=True) min_qty = fields.Float( string="Min Quantity", readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="in the unit of measure for the product") max_qty = fields.Float( string="Max Quantity", readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="in the unit of measure for the product") qty_to_order = fields.Float( string='Quantity to Order', digits=dp.get_precision('Product Unit of Measure'), help="Quantity to order in the purchase unit of measure for the " "product")
class AccountBankStatementLine(models.Model): _name = "account.bank.statement.line" _description = "Bank Statement Line" _order = "statement_id desc, sequence" _inherit = ['ir.needaction_mixin'] name = fields.Char(string='Memo', required=True) date = fields.Date(required=True, default=lambda self: self._context.get('date', fields.Date.context_today(self))) amount = fields.Monetary(digits=0, currency_field='journal_currency_id') journal_currency_id = fields.Many2one('res.currency', related='statement_id.currency_id', help='Utility field to express amount currency', readonly=True) partner_id = fields.Many2one('res.partner', string='Partner') bank_account_id = fields.Many2one('res.partner.bank', string='Bank Account') account_id = fields.Many2one('account.account', string='Counterpart Account', domain=[('deprecated', '=', False)], help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation" " process on it later on. The statement line will simply create a counterpart on this account") statement_id = fields.Many2one('account.bank.statement', string='Statement', index=True, required=True, ondelete='cascade') journal_id = fields.Many2one('account.journal', related='statement_id.journal_id', string='Journal', store=True, readonly=True) partner_name = fields.Char(help="This field is used to record the third party name when importing bank statement in electronic format," " when the partner doesn't exist yet in the database (or cannot be found).") ref = fields.Char(string='Reference') note = fields.Text(string='Notes') sequence = fields.Integer(index=True, help="Gives the sequence order when displaying a list of bank statement lines.", default=1) company_id = fields.Many2one('res.company', related='statement_id.company_id', string='Company', store=True, readonly=True) journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True) amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.") currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.") @api.one @api.constrains('amount') def _check_amount(self): # This constraint could possibly underline flaws in bank statement import (eg. inability to # support hacks such as using dummy transactions to give additional informations) if self.amount == 0: raise ValidationError(_('A transaction can\'t have a 0 amount.')) @api.one @api.constrains('amount', 'amount_currency') def _check_amount_currency(self): if self.amount_currency != 0 and self.amount == 0: raise ValidationError(_('If "Amount Currency" is specified, then "Amount" must be as well.')) @api.multi def unlink(self): for line in self: if line.journal_entry_ids.ids: raise UserError(_('In order to delete a bank statement line, you must first cancel it to delete related journal items.')) return super(AccountBankStatementLine, self).unlink() @api.model def _needaction_domain_get(self): return [('journal_entry_ids', '=', False), ('account_id', '=', False)] @api.multi def button_cancel_reconciliation(self): # TOCKECK : might not behave as expected in case of reconciliations (match statement line with already # registered payment) or partial reconciliations : it will completely remove the existing payment. move_recs = self.env['account.move'] for st_line in self: move_recs = (move_recs | st_line.journal_entry_ids) if move_recs: for move in move_recs: move.line_ids.remove_move_reconcile() move_recs.write({'statement_line_id': False}) move_recs.button_cancel() move_recs.unlink() #################################################### # Reconciliation interface methods #################################################### @api.multi def get_data_for_reconciliation_widget(self, excluded_ids=None): """ Returns the data required to display a reconciliation widget, for each statement line in self """ excluded_ids = excluded_ids or [] ret = [] for st_line in self: aml_recs = st_line.get_reconciliation_proposition(excluded_ids=excluded_ids) target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id rp = aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=st_line.date) excluded_ids += [move_line['id'] for move_line in rp] ret.append({ 'st_line': st_line.get_statement_line_for_reconciliation_widget(), 'reconciliation_proposition': rp }) return ret def get_statement_line_for_reconciliation_widget(self): """ Returns the data required by the bank statement reconciliation widget to display a statement line """ statement_currency = self.journal_id.currency_id or self.journal_id.company_id.currency_id if self.amount_currency and self.currency_id: amount = self.amount_currency amount_currency = self.amount amount_currency_str = amount_currency > 0 and amount_currency or -amount_currency amount_currency_str = formatLang(self.env, amount_currency_str, currency_obj=statement_currency) else: amount = self.amount amount_currency_str = "" amount_str = formatLang(self.env, abs(amount), currency_obj=self.currency_id or statement_currency) data = { 'id': self.id, 'ref': self.ref, 'note': self.note or "", 'name': self.name, 'date': self.date, 'amount': amount, 'amount_str': amount_str, # Amount in the statement line currency 'currency_id': self.currency_id.id or statement_currency.id, 'partner_id': self.partner_id.id, 'journal_id': self.journal_id.id, 'statement_id': self.statement_id.id, 'account_code': self.journal_id.default_debit_account_id.code, 'account_name': self.journal_id.default_debit_account_id.name, 'partner_name': self.partner_id.name, 'communication_partner_name': self.partner_name, 'amount_currency_str': amount_currency_str, # Amount in the statement currency 'has_no_partner': not self.partner_id.id, } if self.partner_id: if amount > 0: data['open_balance_account_id'] = self.partner_id.property_account_receivable_id.id else: data['open_balance_account_id'] = self.partner_id.property_account_payable_id.id return data @api.multi def get_move_lines_for_reconciliation_widget(self, excluded_ids=None, str=False, offset=0, limit=None): """ Returns move lines for the bank statement reconciliation widget, formatted as a list of dicts """ aml_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str, offset=offset, limit=limit) target_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id return aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=self.date) #################################################### # Reconciliation methods #################################################### def get_move_lines_for_reconciliation(self, excluded_ids=None, str=False, offset=0, limit=None, additional_domain=None, overlook_partner=False): """ Return account.move.line records which can be used for bank statement reconciliation. :param excluded_ids: :param str: :param offset: :param limit: :param additional_domain: :param overlook_partner: """ # Blue lines = payment on bank account not assigned to a statement yet reconciliation_aml_accounts = [self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id] domain_reconciliation = ['&', '&', ('statement_id', '=', False), ('account_id', 'in', reconciliation_aml_accounts), ('payment_id','<>', False)] # Black lines = unreconciled & (not linked to a payment or open balance created by statement domain_matching = ['&', ('reconciled', '=', False), '|', ('payment_id','=',False), ('statement_id', '<>', False)] if self.partner_id.id or overlook_partner: domain_matching = expression.AND([domain_matching, [('account_id.internal_type', 'in', ['payable', 'receivable'])]]) else: # TODO : find out what use case this permits (match a check payment, registered on a journal whose account type is other instead of liquidity) domain_matching = expression.AND([domain_matching, [('account_id.reconcile', '=', True)]]) # Let's add what applies to both domain = expression.OR([domain_reconciliation, domain_matching]) if self.partner_id.id and not overlook_partner: domain = expression.AND([domain, [('partner_id', '=', self.partner_id.id)]]) # Domain factorized for all reconciliation use cases ctx = dict(self._context or {}) ctx['bank_statement_line'] = self generic_domain = self.env['account.move.line'].with_context(ctx).domain_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str) domain = expression.AND([domain, generic_domain]) # Domain from caller if additional_domain is None: additional_domain = [] else: additional_domain = expression.normalize_domain(additional_domain) domain = expression.AND([domain, additional_domain]) return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") def _get_domain_maker_move_line_amount(self): """ Returns a function that can create the appropriate domain to search on move.line amount based on statement.line currency/amount """ company_currency = self.journal_id.company_id.currency_id st_line_currency = self.currency_id or self.journal_id.currency_id currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False field = currency and 'amount_residual_currency' or 'amount_residual' precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places def ret(comparator, amount, p=precision, f=field, c=currency): if comparator == '<': if amount < 0: domain = [(f, '<', 0), (f, '>', amount)] else: domain = [(f, '>', 0), (f, '<', amount)] elif comparator == '=': domain = [(f, '=', float_round(amount, precision_digits=p))] else: raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'")) domain += [('currency_id', '=', c)] return domain return ret def get_reconciliation_proposition(self, excluded_ids=None): """ Returns move lines that constitute the best guess to reconcile a statement line Note: it only looks for move lines in the same currency as the statement line. """ # Look for structured communication match if self.name: overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway domain = [('ref', '=', self.name)] match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) if match_recs and len(match_recs) == 1: return match_recs elif len(match_recs) == 0: move = self.env['account.move'].search([('name', '=', self.name)], limit=1) if move: domain = [('move_id', '=', move.id)] match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) if match_recs and len(match_recs) == 1: return match_recs # How to compare statement line amount and move lines amount amount_domain_maker = self._get_domain_maker_move_line_amount() amount = self.amount_currency or self.amount # Look for a single move line with the same amount match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=1, additional_domain=amount_domain_maker('=', amount)) if match_recs: return match_recs if not self.partner_id: return self.env['account.move.line'] # Select move lines until their total amount is greater than the statement line amount domain = [('reconciled', '=', False)] domain += [('account_id.user_type_id.type', '=', amount > 0 and 'receivable' or 'payable')] # Make sure we can't mix receivable and payable domain += amount_domain_maker('<', amount) # Will also enforce > 0 mv_lines = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=5, additional_domain=domain) st_line_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id ret = self.env['account.move.line'] total = 0 for line in mv_lines: total += line.currency_id and line.amount_residual_currency or line.amount_residual if float_compare(total, abs(amount), precision_digits=st_line_currency.rounding) != -1: break ret = (ret | line) return ret def _get_move_lines_for_auto_reconcile(self): """ Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """ pass @api.multi def auto_reconcile(self): """ Try to automatically reconcile the statement.line ; return the counterpart journal entry/ies if the automatic reconciliation succeeded, False otherwise. TODO : this method could be greatly improved and made extensible """ self.ensure_one() match_recs = self.env['account.move.line'] # How to compare statement line amount and move lines amount amount_domain_maker = self._get_domain_maker_move_line_amount() equal_amount_domain = amount_domain_maker('=', self.amount_currency or self.amount) # Look for structured communication match if self.name: overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway domain = equal_amount_domain + [('ref', '=', self.name)] match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=domain, overlook_partner=overlook_partner) if match_recs and len(match_recs) != 1: return False # Look for a single move line with the same partner, the same amount if not match_recs: if self.partner_id: match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=equal_amount_domain) if match_recs and len(match_recs) != 1: return False if not match_recs: return False # Now reconcile counterpart_aml_dicts = [] payment_aml_rec = self.env['account.move.line'] for aml in match_recs: if aml.account_id.internal_type == 'liquidity': payment_aml_rec = (payment_aml_rec | aml) else: amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual counterpart_aml_dicts.append({ 'name': aml.name if aml.name != '/' else aml.move_id.name, 'debit': amount < 0 and -amount or 0, 'credit': amount > 0 and amount or 0, 'move_line': aml }) try: with self._cr.savepoint(): counterpart = self.process_reconciliation(counterpart_aml_dicts=counterpart_aml_dicts, payment_aml_rec=payment_aml_rec) return counterpart except UserError: # A configuration / business logic error that makes it impossible to auto-reconcile should not be raised # since automatic reconciliation is just an amenity and the user will get the same exception when manually # reconciling. Other types of exception are (hopefully) programmation errors and should cause a stacktrace. self.invalidate_cache() self.env['account.move'].invalidate_cache() self.env['account.move.line'].invalidate_cache() return False def _prepare_reconciliation_move(self, move_name): """ Prepare the dict of values to create the move from a statement line. This method may be overridden to adapt domain logic through model inheritance (make sure to call super() to establish a clean extension chain). :param char st_line_number: will be used as the name of the generated account move :return: dict of value to create() the account.move """ return { 'statement_line_id': self.id, 'journal_id': self.statement_id.journal_id.id, 'date': self.date, 'name': move_name, 'ref': self.ref, } def _prepare_reconciliation_move_line(self, move, amount): """ Prepare the dict of values to create the move line from a statement line. :param recordset move: the account.move to link the move line :param float amount: the amount of transaction that wasn't already reconciled """ company_currency = self.journal_id.company_id.currency_id statement_currency = self.journal_id.currency_id or company_currency st_line_currency = self.currency_id or statement_currency amount_currency = False if statement_currency != company_currency or st_line_currency != company_currency: # First get the ratio total mount / amount not already reconciled if statement_currency == company_currency: total_amount = self.amount elif st_line_currency == company_currency: total_amount = self.amount_currency else: total_amount = statement_currency.with_context({'date': self.date}).compute(self.amount, company_currency) ratio = total_amount / amount # Then use it to adjust the statement.line field that correspond to the move.line amount_currency if statement_currency != company_currency: amount_currency = self.amount * ratio elif st_line_currency != company_currency: amount_currency = self.amount_currency * ratio return { 'name': self.name, 'date': self.date, 'ref': self.ref, 'move_id': move.id, 'partner_id': self.partner_id and self.partner_id.id or False, 'account_id': amount >= 0 \ and self.statement_id.journal_id.default_credit_account_id.id \ or self.statement_id.journal_id.default_debit_account_id.id, 'credit': amount < 0 and -amount or 0.0, 'debit': amount > 0 and amount or 0.0, 'statement_id': self.statement_id.id, 'journal_id': self.statement_id.journal_id.id, 'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False), 'amount_currency': amount_currency, } @api.v7 def process_reconciliations(self, cr, uid, ids, data, context=None): """ Handles data sent from the bank statement reconciliation widget (and can otherwise serve as an old-API bridge) :param list of dicts data: must contains the keys 'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts', whose value is the same as described in process_reconciliation except that ids are used instead of recordsets. """ aml_obj = self.pool['account.move.line'] for id, datum in zip(ids, data): st_line = self.browse(cr, uid, id, context) payment_aml_rec = aml_obj.browse(cr, uid, datum.get('payment_aml_ids', []), context) for aml_dict in datum.get('counterpart_aml_dicts', []): aml_dict['move_line'] = aml_obj.browse(cr, uid, aml_dict['counterpart_aml_id'], context) del aml_dict['counterpart_aml_id'] st_line.process_reconciliation(datum.get('counterpart_aml_dicts', []), payment_aml_rec, datum.get('new_aml_dicts', [])) def fast_counterpart_creation(self): for st_line in self: # Technical functionality to automatically reconcile by creating a new move line vals = { 'name': st_line.name, 'debit': st_line.amount < 0 and -st_line.amount or 0.0, 'credit': st_line.amount > 0 and st_line.amount or 0.0, 'account_id': st_line.account_id.id, } st_line.process_reconciliation(new_aml_dicts=[vals]) def process_reconciliation(self, counterpart_aml_dicts=None, payment_aml_rec=None, new_aml_dicts=None): """ Match statement lines with existing payments (eg. checks) and/or payables/receivables (eg. invoices and refunds) and/or new move lines (eg. write-offs). If any new journal item needs to be created (via new_aml_dicts or counterpart_aml_dicts), a new journal entry will be created and will contain those items, as well as a journal item for the bank statement line. Finally, mark the statement line as reconciled by putting the matched moves ids in the column journal_entry_ids. :param (list of dicts) counterpart_aml_dicts: move lines to create to reconcile with existing payables/receivables. The expected keys are : - 'name' - 'debit' - 'credit' - 'move_line' # The move line to reconcile (partially if specified debit/credit is lower than move line's credit/debit) :param (list of recordsets) payment_aml_rec: recordset move lines representing existing payments (which are already fully reconciled) :param (list of dicts) new_aml_dicts: move lines to create. The expected keys are : - 'name' - 'debit' - 'credit' - 'account_id' - (optional) 'tax_ids' - (optional) Other account.move.line fields like analytic_account_id or analytics_id :returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*). """ counterpart_aml_dicts = counterpart_aml_dicts or [] payment_aml_rec = payment_aml_rec or self.env['account.move.line'] new_aml_dicts = new_aml_dicts or [] aml_obj = self.env['account.move.line'] company_currency = self.journal_id.company_id.currency_id statement_currency = self.journal_id.currency_id or company_currency st_line_currency = self.currency_id or statement_currency counterpart_moves = self.env['account.move'] # Check and prepare received data if self.journal_entry_ids.ids: raise UserError(_('The bank statement line was already reconciled.')) if any(rec.statement_id for rec in payment_aml_rec): raise UserError(_('A selected move line was already reconciled.')) for aml_dict in counterpart_aml_dicts: if aml_dict['move_line'].reconciled: raise UserError(_('A selected move line was already reconciled.')) if isinstance(aml_dict['move_line'], (int, long)): aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line']) for aml_dict in (counterpart_aml_dicts + new_aml_dicts): if aml_dict.get('tax_ids') and aml_dict['tax_ids'] and isinstance(aml_dict['tax_ids'][0], (int, long)): # Transform the value in the format required for One2many and Many2many fields aml_dict['tax_ids'] = map(lambda id: (4, id, None), aml_dict['tax_ids']) # Fully reconciled moves are just linked to the bank statement total = self.amount for aml_rec in payment_aml_rec: total -= aml_rec.debit-aml_rec.credit aml_rec.write({'statement_id': self.statement_id.id}) aml_rec.move_id.write({'statement_line_id': self.id}) counterpart_moves = (counterpart_moves | aml_rec.move_id) # Create move line(s). Either matching an existing journal entry (eg. invoice), in which # case we reconcile the existing and the new move lines together, or being a write-off. if counterpart_aml_dicts or new_aml_dicts: st_line_currency = self.currency_id or statement_currency st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False # Create the move move_name = (self.statement_id.name or self.name) + "/" + str(self.sequence) move_vals = self._prepare_reconciliation_move(move_name) move = self.env['account.move'].create(move_vals) counterpart_moves = (counterpart_moves | move) # Create The payment payment_id = False if abs(total)>0.00001: partner_id = self.partner_id and self.partner_id.id or False partner_type = False if partner_id: if total < 0: partner_type = 'supplier' else: partner_type = 'customer' payment_methods = (total>0) and self.journal_id.inbound_payment_method_ids or self.journal_id.outbound_payment_method_ids currency = self.journal_id.currency_id or self.company_id.currency_id payment_id = self.env['account.payment'].create({ 'payment_method_id': payment_methods and payment_methods[0].id or False, 'payment_type': total >0 and 'inbound' or 'outbound', 'partner_id': self.partner_id and self.partner_id.id or False, 'partner_type': partner_type, 'journal_id': self.statement_id.journal_id.id, 'payment_date': self.date, 'state': 'reconciled', 'currency_id': currency.id, 'amount': abs(total), 'communication': self.name or '', 'name': self.statement_id.name, }).id # Complete dicts to create both counterpart move lines and write-offs to_create = (counterpart_aml_dicts + new_aml_dicts) ctx = dict(self._context, date=self.date) for aml_dict in to_create: aml_dict['move_id'] = move.id aml_dict['partner_id'] = self.partner_id.id aml_dict['statement_id'] = self.statement_id.id if st_line_currency.id != company_currency.id: aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit'] aml_dict['currency_id'] = st_line_currency.id if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: # Statement is in company currency but the transaction is in foreign currency aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate) aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate) elif self.currency_id and st_line_currency_rate: # Statement is in foreign currency and the transaction is in another one aml_dict['debit'] = statement_currency.with_context(ctx).compute(aml_dict['debit'] / st_line_currency_rate, company_currency) aml_dict['credit'] = statement_currency.with_context(ctx).compute(aml_dict['credit'] / st_line_currency_rate, company_currency) else: # Statement is in foreign currency and no extra currency is given for the transaction aml_dict['debit'] = st_line_currency.with_context(ctx).compute(aml_dict['debit'], company_currency) aml_dict['credit'] = st_line_currency.with_context(ctx).compute(aml_dict['credit'], company_currency) elif statement_currency.id != company_currency.id: # Statement is in foreign currency but the transaction is in company currency prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency aml_dict['amount_currency'] = prorata_factor * self.amount aml_dict['currency_id'] = statement_currency.id # Create the move line for the statement line using the total credit/debit of the counterpart # This leaves out the amount already reconciled and avoids rounding errors from currency conversion st_line_amount = sum(aml_dict['credit'] - aml_dict['debit'] for aml_dict in to_create) aml_dict = self._prepare_reconciliation_move_line(move, st_line_amount) aml_dict['payment_id'] = payment_id aml_obj.with_context(check_move_validity=False).create(aml_dict) # Create write-offs for aml_dict in new_aml_dicts: aml_dict['payment_id'] = payment_id aml_obj.with_context(check_move_validity=False).create(aml_dict) # Create counterpart move lines and reconcile them for aml_dict in counterpart_aml_dicts: if aml_dict['move_line'].partner_id.id: aml_dict['partner_id'] = aml_dict['move_line'].partner_id.id aml_dict['account_id'] = aml_dict['move_line'].account_id.id aml_dict['payment_id'] = payment_id counterpart_move_line = aml_dict.pop('move_line') if counterpart_move_line.currency_id and counterpart_move_line.currency_id != company_currency and not aml_dict.get('currency_id'): aml_dict['currency_id'] = counterpart_move_line.currency_id.id aml_dict['amount_currency'] = company_currency.with_context(ctx).compute(aml_dict['debit'] - aml_dict['credit'], counterpart_move_line.currency_id) new_aml = aml_obj.with_context(check_move_validity=False).create(aml_dict) (new_aml | counterpart_move_line).reconcile() move.post() counterpart_moves.assert_balanced() return counterpart_moves
class ResPartner(models.Model): _name = 'res.partner' _inherit = 'res.partner' _description = 'Partner' @api.multi def _credit_debit_get(self): tables, where_clause, where_params = self.env[ 'account.move.line']._query_get() where_params = [tuple(self.ids)] + where_params self._cr.execute( """SELECT l.partner_id, act.type, SUM(l.amount_residual) FROM account_move_line l LEFT JOIN account_account a ON (l.account_id=a.id) LEFT JOIN account_account_type act ON (a.user_type_id=act.id) WHERE act.type IN ('receivable','payable') AND l.partner_id IN %s AND l.reconciled IS FALSE """ + where_clause + """ GROUP BY l.partner_id, act.type """, where_params) for pid, type, val in self._cr.fetchall(): partner = self.browse(pid) if type == 'receivable': partner.credit = val elif type == 'payable': partner.debit = -val @api.multi def _asset_difference_search(self, type, args): if not args: return [] having_values = tuple(map(itemgetter(2), args)) where = ' AND '.join( map(lambda x: '(SUM(bal2) %(operator)s %%s)' % {'operator': x[1]}, args)) query = self.env['account.move.line']._query_get() self._cr.execute(('SELECT pid AS partner_id, SUM(bal2) FROM ' \ '(SELECT CASE WHEN bal IS NOT NULL THEN bal ' \ 'ELSE 0.0 END AS bal2, p.id as pid FROM ' \ '(SELECT (debit-credit) AS bal, partner_id ' \ 'FROM account_move_line l ' \ 'WHERE account_id IN ' \ '(SELECT id FROM account_account '\ 'WHERE type=%s AND active) ' \ 'AND reconciled IS FALSE ' \ 'AND '+query+') AS l ' \ 'RIGHT JOIN res_partner p ' \ 'ON p.id = partner_id ) AS pl ' \ 'GROUP BY pid HAVING ' + where), (type,) + having_values) res = self._cr.fetchall() if not res: return [('id', '=', '0')] return [('id', 'in', map(itemgetter(0), res))] @api.multi def _credit_search(self, args): return self._asset_difference_search('receivable', args) @api.multi def _debit_search(self, args): return self._asset_difference_search('payable', args) @api.multi def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: self.total_invoiced = 0.0 return True user_currency_id = self.env.user.company_id.currency_id.id for partner in self: all_partner_ids = self.search([('id', 'child_of', partner.id)]).ids # searching account.invoice.report via the orm is comparatively expensive # (generates queries "id in []" forcing to build the full table). # In simple cases where all invoices are in the same currency than the user's company # access directly these elements # generate where clause to include multicompany rules where_query = account_invoice_report._where_calc([ ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id) ]) account_invoice_report._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql( ) query = """ SELECT SUM(price_total) as total FROM account_invoice_report account_invoice_report WHERE %s """ % where_clause # price_total is in the company currency self.env.cr.execute(query, where_clause_params) partner.total_invoiced = self.env.cr.fetchone()[0] @api.multi def _journal_item_count(self): for partner in self: partner.journal_item_count = self.env[ 'account.move.line'].search_count([('partner_id', '=', partner.id)]) partner.contracts_count = self.env[ 'account.analytic.account'].search_count([('partner_id', '=', partner.id)]) def get_followup_lines_domain(self, date, overdue_only=False, only_unblocked=False): domain = [('reconciled', '=', False), ('account_id.deprecated', '=', False), ('account_id.internal_type', '=', 'receivable')] if only_unblocked: domain += [('blocked', '=', False)] if self.ids: if 'exclude_given_ids' in self._context: domain += [('partner_id', 'not in', self.ids)] else: domain += [('partner_id', 'in', self.ids)] #adding the overdue lines overdue_domain = [ '|', '&', ('date_maturity', '!=', False), ('date_maturity', '<=', date), '&', ('date_maturity', '=', False), ('date', '<=', date) ] if overdue_only: domain += overdue_domain return domain @api.multi def _compute_issued_total(self): """ Returns the issued total as will be displayed on partner view """ today = fields.Date.context_today(self) for partner in self: domain = partner.get_followup_lines_domain(today, overdue_only=True) issued_total = 0 for aml in self.env['account.move.line'].search(domain): issued_total += aml.amount_residual partner.issued_total = issued_total @api.one def _compute_has_unreconciled_entries(self): # Avoid useless work if has_unreconciled_entries is not relevant for this partner if not self.active or not self.is_company and self.parent_id: return self.env.cr.execute( """ SELECT 1 FROM( SELECT p.last_time_entries_checked AS last_time_entries_checked, MAX(l.write_date) AS max_date FROM account_move_line l RIGHT JOIN account_account a ON (a.id = l.account_id) RIGHT JOIN res_partner p ON (l.partner_id = p.id) WHERE p.id = %s AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual > 0 ) AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual < 0 ) GROUP BY p.last_time_entries_checked ) as s WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) """, (self.id, )) self.has_unreconciled_entries = self.env.cr.rowcount == 1 @api.multi def mark_as_reconciled(self): return self.write({ 'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) }) @api.one def _get_company_currency(self): if self.company_id: self.currency_id = self.sudo().company_id.currency_id else: self.currency_id = self.env.user.company_id.currency_id credit = fields.Monetary(compute='_credit_debit_get', search=_credit_search, string='Total Receivable', help="Total amount this customer owes you.") debit = fields.Monetary( compute='_credit_debit_get', search=_debit_search, string='Total Payable', help="Total amount you have to pay to this vendor.") debit_limit = fields.Monetary('Payable Limit') total_invoiced = fields.Monetary(compute='_invoice_total', string="Total Invoiced", groups='account.group_account_invoice') currency_id = fields.Many2one( 'res.currency', compute='_get_company_currency', readonly=True, help='Utility field to express amount currency') contracts_count = fields.Integer(compute='_journal_item_count', string="Contracts", type='integer') journal_item_count = fields.Integer(compute='_journal_item_count', string="Journal Items", type="integer") issued_total = fields.Monetary(compute='_compute_issued_total', string="Journal Items") property_account_payable_id = fields.Many2one( 'account.account', company_dependent=True, string="Account Payable", oldname="property_account_payable", domain= "[('internal_type', '=', 'payable'), ('deprecated', '=', False)]", help= "This account will be used instead of the default one as the payable account for the current partner", required=True) property_account_receivable_id = fields.Many2one( 'account.account', company_dependent=True, string="Account Receivable", oldname="property_account_receivable", domain= "[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]", help= "This account will be used instead of the default one as the receivable account for the current partner", required=True) property_account_position_id = fields.Many2one( 'account.fiscal.position', company_dependent=True, string="Fiscal Position", help= "The fiscal position will determine taxes and accounts used for the partner.", oldname="property_account_position") property_payment_term_id = fields.Many2one( 'account.payment.term', company_dependent=True, string='Customer Payment Term', help= "This payment term will be used instead of the default one for sale orders and customer invoices", oldname="property_payment_term") property_supplier_payment_term_id = fields.Many2one( 'account.payment.term', company_dependent=True, string='Vendor Payment Term', help= "This payment term will be used instead of the default one for purchase orders and vendor bills", oldname="property_supplier_payment_term") ref_company_ids = fields.One2many( 'res.company', 'partner_id', string='Companies that refers to partner', oldname="ref_companies") has_unreconciled_entries = fields.Boolean( compute='_compute_has_unreconciled_entries', help= "The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed." ) last_time_entries_checked = fields.Datetime( oldname='last_reconciliation_date', string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, help= 'Last time the invoices & payments matching was performed for this partner. ' 'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit ' 'or if you click the "Done" button.') invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices', readonly=True, copy=False) contract_ids = fields.One2many('account.analytic.account', 'partner_id', string='Contracts', readonly=True) bank_account_count = fields.Integer(compute='_compute_bank_count', string="Bank") trust = fields.Selection([('good', 'Good Debtor'), ('normal', 'Normal Debtor'), ('bad', 'Bad Debtor')], string='Degree of trust you have in this debtor', default='normal', company_dependent=True) @api.multi def _compute_bank_count(self): bank_data = self.env['res.partner.bank'].read_group( [('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count']) for bank in bank_data]) for partner in self: partner.bank_account_count = mapped_data.get(partner.id, 0) def _find_accounting_partner(self, partner): ''' Find the partner for which the accounting entries will be created ''' return partner.commercial_partner_id @api.model def _commercial_fields(self): return super(ResPartner, self)._commercial_fields() + \ ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id', 'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']
class product_supplier_info(models.Model): _inherit = 'product.supplierinfo' @api.depends('attribute_value_ids') def _get_price_extra_percentage(self): product_id = self.env.context and self.env.context.get( 'product_id') or False for supplier in self: price_extra = 0.0 for variant_id in supplier.attribute_value_ids: if product_id and variant_id.value not in product_id.attribute_value_ids: continue if variant_id.price_extra_type != 'percentage' or supplier.id != variant_id.supplierinfo_id.id: continue price_extra += variant_id.price_extra supplier.price_extra_perc = price_extra @api.depends('attribute_value_ids') def _get_price_extra(self): product_id = self.env.context and self.env.context.get( 'product_id') or False for supplier in self: price_extra = 0.0 for variant_id in supplier.attribute_value_ids: if product_id and variant_id.value not in product_id.attribute_value_ids: continue if variant_id.price_extra_type != 'standard' or supplier.id != variant_id.supplierinfo_id.id: continue price_extra += variant_id.price_extra supplier.price_extra = price_extra price_area_min_width = fields.Float( string="Min. Width", default=0.0, digits=dp.get_precision('Product Price')) price_area_max_width = fields.Float( string="Max. Width", default=0.0, digits=dp.get_precision('Product Price')) price_area_min_height = fields.Float( string="Min. Height", default=0.0, digits=dp.get_precision('Product Price')) price_area_max_height = fields.Float( string="Max. Height", default=0.0, digits=dp.get_precision('Product Price')) min_price_area = fields.Monetary('Min. Price') price_type = fields.Selection( PRICE_TYPES, string='Supplier Price Type', required=True, default='standard', ) prices_table = fields.One2many('product.prices_table', 'supplier_product_id', string="Supplier Prices Table") attribute_value_ids = fields.One2many( comodel_name='supplier.attribute.value', inverse_name='supplierinfo_id') price_extra = fields.Float( compute='_get_price_extra', string='Variant Extra Price', help="This is the sum of the extra price of all attributes", digits_compute=dp.get_precision('Product Price')) price_extra_perc = fields.Float( compute='_get_price_extra_percentage', string='Variant Extra Price Percentage', help="This is the percentage of the extra price of all attributes", digits_compute=dp.get_precision('Product Price')) def get_price_table_headers(self): result = {'x': [0], 'y': [0]} for rec in self.prices_table: result['x'].append(rec.pos_x) result['y'].append(rec.pos_y) result.update({ 'x': sorted(list(set(result['x']))), 'y': sorted(list(set(result['y']))) }) return result # TODO: Estos metodos necesitarán ser reescritos cuando se usen los atributos def manzano_check_dim_values(self, width, height): if self.price_type in ['table_1d', 'table_2d']: product_prices_table_obj = self.env['product.prices_table'] norm_width = self.manzano_normalize_width_value(width) if self.price_type == 'table_2d': norm_height = self.manzano_normalize_height_value(height) return product_prices_table_obj.search_count( [('supplier_product_id', '=', self.id), ('pos_x', '=', norm_width), ('pos_y', '=', norm_height), ('value', '!=', 0)]) > 0 return product_prices_table_obj.search_count( [('supplier_product_id', '=', self.id), ('pos_x', '=', norm_width), ('value', '!=', 0)]) > 0 elif self.price_type == 'area': return width >= self.price_area_min_width and width <= self.price_area_max_width and height >= self.price_area_min_height and height <= self.price_area_max_height return True def manzano_normalize_width_value(self, width): headers = self.get_price_table_headers() norm_val = width for index in range(len(headers['x']) - 1): if width > headers['x'][index] and width <= headers['x'][index + 1]: norm_val = headers['x'][index + 1] return norm_val def manzano_normalize_height_value(self, height): headers = self.get_price_table_headers() norm_val = height for index in range(len(headers['y']) - 1): if height > headers['y'][index] and height <= headers['y'][index + 1]: norm_val = headers['y'][index + 1] return norm_val @api.depends('price') def get_supplier_price(self): # FIXME: Mejor usar atributos manzano_width = self.env.context and self.env.context.get( 'width') or False manzano_height = self.env.context and self.env.context.get( 'height') or False product_id = self.env.context and self.env.context.get( 'product_id') or False result = {} for record in self: result[record.id] = False if manzano_width: product_prices_table_obj = self.env['product.prices_table'] manzano_width = record.manzano_normalize_width_value( manzano_width) if record.price_type == 'table_2d': manzano_height = record.manzano_normalize_height_value( manzano_height) res = product_prices_table_obj.search( [('supplier_product_id', '=', record.id), ('pos_x', '=', manzano_width), ('pos_y', '=', manzano_height)], limit=1) result[record.id] = res and res.value or False elif record.price_type == 'table_1d': res = product_prices_table_obj.search( [('supplier_product_id', '=', record.id), ('pos_x', '=', manzano_width)], limit=1) result[record.id] = res and res.value or False elif record.price_type == 'area': result[record. id] = record.price * manzano_width * manzano_height result[record.id] = max(record.min_price_area, result[record.id]) if not result[record.id]: result[record.id] = record.price result[record.id] += record.with_context( product_id=product_id).price_extra + record.with_context( product_id=product_id).price_extra_perc return result # --- # -- START Original source by 'Prev. Manzano Dev.' @api.multi def action_open_value_extras(self): self.ensure_one() extra_ds = self.env['supplier.attribute.value'] for line in self.product_tmpl_id.attribute_line_ids: for value in line.value_ids: extra = extra_ds.search([('supplierinfo_id', '=', self.id), ('value', '=', value.id)]) if not extra: extra = extra_ds.create({ 'supplierinfo_id': self.id, 'value': value.id, }) extra_ds |= extra all_supplierinfo_extra = extra_ds.search([('supplierinfo_id', '=', self.id)]) remove_extra = all_supplierinfo_extra - extra_ds remove_extra.unlink() result = self.product_tmpl_id._get_act_window_dict( 'price_dimension.supplier_attribute_value_action') return result