# -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. from flectra import fields, models, api, _ from flectra.addons import decimal_precision as dp from flectra.exceptions import UserError, ValidationError class AccountVoucher(models.Model): _name = 'account.voucher' _description = 'Accounting Voucher' _inherit = ['mail.thread'] _order = "date desc, id desc" @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) 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("Bill Date", readonly=True, index=True, states={'draft': [('readonly', False)]}, copy=False, default=fields.Date.context_today) account_date = fields.Date("Accounting Date", readonly=True, index=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) payment_journal_id = fields.Many2one('account.journal', string='Payment Method', readonly=True, store=False, states={'draft': [('readonly', False)]}, domain="[('type', 'in', ['cash', 'bank'])]", compute='_compute_payment_journal_id', inverse='_inverse_payment_journal_id') account_id = fields.Many2one('account.account', 'Account', required=True, readonly=True, states={'draft': [('readonly', False)]}, domain="[('deprecated', '=', False), ('internal_type','=', (pay_now == 'pay_now' and 'liquidity' or voucher_type == 'purchase' and 'payable' or 'receivable'))]") 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', store=True, required=True, readonly=True, states={'draft': [('readonly', False)]}, related='journal_id.company_id', default=lambda self: self._get_company()) 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' status is used when the voucher does not have a 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', index=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_later') date_due = fields.Date('Due Date', readonly=True, index=True, states={'draft': [('readonly', False)]}) branch_id = fields.Many2one('res.branch', 'Branch', ondelete="restrict", default=lambda self: self.env['res.users']._get_default_branch()) @api.constrains('company_id', 'branch_id') def _check_company_branch(self): for record in self: if record.branch_id and record.company_id != record.branch_id.company_id: raise ValidationError( _('Configuration Error of Company:\n' 'The Company (%s) in the voucher and ' 'the Company (%s) of Branch must ' 'be the same company!') % (record.company_id.name, record.branch_id.company_id.name) ) @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.env.context.get('default_journal_id', False)) if journal.currency_id: return journal.currency_id.id return self.env.user.company_id.currency_id.id @api.model def _get_company(self): return self._context.get('company_id', self.env.user.company_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.depends('company_id', 'pay_now', 'account_id') def _compute_payment_journal_id(self): for voucher in self: if voucher.pay_now != 'pay_now': continue domain = [ ('type', 'in', ('bank', 'cash')), ('company_id', '=', voucher.company_id.id), ] if voucher.account_id and voucher.account_id.internal_type == 'liquidity': field = 'default_debit_account_id' if voucher.voucher_type == 'sale' else 'default_credit_account_id' domain.append((field, '=', voucher.account_id.id)) voucher.payment_journal_id = self.env['account.journal'].search(domain, limit=1) def _inverse_payment_journal_id(self): for voucher in self: if voucher.pay_now != 'pay_now': continue if voucher.voucher_type == 'sale': voucher.account_id = voucher.payment_journal_id.default_debit_account_id else: voucher.account_id = voucher.payment_journal_id.default_credit_account_id @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, voucher.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 @api.onchange('date') def onchange_date(self): self.account_date = self.date @api.onchange('partner_id', 'pay_now') def onchange_partner_id(self): pay_journal_domain = [('type', 'in', ['cash', 'bank'])] if self.pay_now != 'pay_now': if self.partner_id: self.account_id = self.partner_id.property_account_receivable_id \ if self.voucher_type == 'sale' else self.partner_id.property_account_payable_id else: account_type = self.voucher_type == 'purchase' and 'payable' or 'receivable' domain = [('deprecated', '=', False), ('internal_type', '=', account_type)] self.account_id = self.env['account.account'].search(domain, limit=1) else: if self.voucher_type == 'purchase': pay_journal_domain.append(('outbound_payment_method_ids', '!=', False)) else: pay_journal_domain.append(('inbound_payment_method_ids', '!=', False)) return {'domain': {'payment_journal_id': pay_journal_domain}} @api.multi def proforma_voucher(self): self.action_move_line_create() @api.multi def action_cancel_draft(self): 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 UserError(_('Cannot delete voucher(s) which are already opened or paid.')) return super(AccountVoucher, self).unlink() @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.commercial_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.account_date, 'date_maturity': self.date_due, 'payment_id': self._context.get('payment_id'), 'branch_id': self.branch_id.id, } 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.account_date, 'ref': self.reference, 'branch_id': self.branch_id.id } 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_pay_now_payment_create(self): if self.voucher_type == 'sale': payment_methods = self.journal_id.inbound_payment_method_ids payment_type = 'inbound' partner_type = 'customer' sequence_code = 'account.payment.customer.invoice' else: payment_methods = self.journal_id.outbound_payment_method_ids payment_type = 'outbound' partner_type = 'supplier' sequence_code = 'account.payment.supplier.invoice' name = self.env['ir.sequence'].with_context(ir_sequence_date=self.date).next_by_code(sequence_code) return { 'name': name, 'payment_type': payment_type, 'payment_method_id': payment_methods and payment_methods[0].id or False, 'partner_type': partner_type, 'partner_id': self.partner_id.commercial_partner_id.id, 'amount': self.amount, 'currency_id': self.currency_id.id, 'payment_date': self.date, 'journal_id': self.payment_journal_id.id, 'company_id': self.company_id.id, 'communication': self.name, 'state': 'reconciled', } @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) ''' for line in self.line_ids: #create one move line per voucher line where amount is not 0.0 if not line.price_subtotal: continue line_subtotal = line.price_subtotal if self.voucher_type == 'sale': line_subtotal = -1 * line.price_subtotal # 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.commercial_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.account_date, 'tax_ids': [(4,t.id) for t in line.tax_ids], 'amount_currency': line_subtotal if current_currency != company_currency else 0.0, 'currency_id': company_currency != current_currency and current_currency or False, 'payment_id': self._context.get('payment_id'), } self.env['account.move.line'].with_context(apply_taxes=True).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.account_date ctx['check_move_validity'] = False # Create a payment to allow the reconciliation when pay_now = 'pay_now'. if self.pay_now == 'pay_now' and self.amount > 0: ctx['payment_id'] = self.env['account.payment'].create(self.voucher_pay_now_payment_create()).id # 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.with_context(ctx).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 AccountVoucherLine(models.Model): _name = 'account.voucher.line' _description = 'Voucher Lines' 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.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'), oldname='amount') 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_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') branch_id = fields.Many2one('res.branch', related='voucher_id.branch_id', string='Branch', readonly=True, store=True) @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'] @api.onchange('product_id', 'voucher_id', 'price_unit', 'company_id') def _onchange_line_details(self): if not self.voucher_id or not self.product_id or not self.voucher_id.partner_id: return onchange_res = self.product_id_change( self.product_id.id, self.voucher_id.partner_id.id, self.price_unit, self.company_id.id, self.voucher_id.currency_id.id, self.voucher_id.voucher_type) for fname, fvalue in onchange_res['value'].items(): setattr(self, fname, fvalue) 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): # TDE note: mix of old and new onchange badly written in 9, multi but does not use record set 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 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'] = price_unit or 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'] = price_unit or product.standard_price values['price_unit'] = values['price_unit'] * currency.rate return {'value': values, 'domain': {}}