# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. from decimal import Decimal from collections import defaultdict, namedtuple from itertools import combinations from sql import Null from sql.aggregate import Sum from sql.conditionals import Coalesce, Case from sql.functions import Round from trytond.i18n import gettext from trytond.model import Workflow, ModelView, ModelSQL, fields, \ sequence_ordered, Unique, DeactivableMixin, dualmethod from trytond.model.exceptions import AccessError from trytond.report import Report from trytond.wizard import Wizard, StateView, StateTransition, StateAction, \ Button from trytond import backend from trytond.pyson import If, Eval, Bool from trytond.tools import reduce_ids, grouped_slice, firstline from trytond.transaction import Transaction from trytond.pool import Pool from trytond.rpc import RPC from trytond.config import config from trytond.modules.account.tax import TaxableMixin from trytond.modules.product import price_digits from .exceptions import ( InvoiceTaxValidationError, InvoiceNumberError, InvoiceValidationError, InvoiceLineValidationError, PayInvoiceError, InvoicePaymentTermDateWarning) if config.getboolean('account_invoice', 'filestore', default=False): file_id = 'invoice_report_cache_id' store_prefix = config.get('account_invoice', 'store_prefix', default=None) else: file_id = None store_prefix = None class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin): 'Invoice' __name__ = 'account.invoice' _order_name = 'number' _states = { 'readonly': Eval('state') != 'draft', } _depends = ['state'] company = fields.Many2One( 'company.company', 'Company', required=True, select=True, states={ 'readonly': _states['readonly'] | Eval('party', True), }, depends=_depends + ['party']) company_party = fields.Function( fields.Many2One( 'party.party', "Company Party", context={ 'company': Eval('company', -1), }, depends=['company']), 'on_change_with_company_party') tax_identifier = fields.Many2One( 'party.identifier', "Tax Identifier", states=_states, depends=_depends) type = fields.Selection([ ('out', "Customer"), ('in', "Supplier"), ], "Type", select=True, required=True, states={ 'readonly': ((Eval('state') != 'draft') | Eval('context', {}).get('type') | (Eval('lines', [0]) & Eval('type'))), }, depends=['state']) type_name = fields.Function(fields.Char('Type'), 'get_type_name') number = fields.Char('Number', size=None, readonly=True, select=True) reference = fields.Char('Reference', size=None, states=_states, depends=_depends) description = fields.Char('Description', size=None, states=_states, depends=_depends) state = fields.Selection([ ('draft', "Draft"), ('validated', "Validated"), ('posted', "Posted"), ('paid', "Paid"), ('cancelled', "Cancelled"), ], "State", readonly=True) invoice_date = fields.Date('Invoice Date', states={ 'readonly': Eval('state').in_(['posted', 'paid', 'cancelled']), 'required': Eval('state').in_( If(Eval('type') == 'in', ['validated', 'posted', 'paid'], ['posted', 'paid'])), }, depends=['state']) accounting_date = fields.Date('Accounting Date', states=_states, depends=_depends) payment_term_date = fields.Date( "Payment Term Date", states=_states, depends=_depends, help="The date from which the payment term is calculated.\n" "Leave empty to use the invoice date.") sequence = fields.Integer("Sequence", readonly=True) party = fields.Many2One( 'party.party', 'Party', required=True, states=_states, context={ 'company': Eval('company', -1), }, depends=_depends + ['company']) party_tax_identifier = fields.Many2One( 'party.identifier', "Party Tax Identifier", states=_states, domain=[ ('party', '=', Eval('party', -1)), ], depends=_depends + ['party']) party_lang = fields.Function(fields.Char('Party Language'), 'on_change_with_party_lang') invoice_address = fields.Many2One('party.address', 'Invoice Address', required=True, states=_states, depends=_depends + ['party'], domain=[('party', '=', Eval('party'))]) currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': ( _states['readonly'] | (Eval('lines', [0]) & Eval('currency'))), }, depends=_depends) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') currency_date = fields.Function(fields.Date('Currency Date'), 'on_change_with_currency_date') journal = fields.Many2One( 'account.journal', 'Journal', required=True, states=_states, context={ 'company': Eval('company', -1), }, depends=_depends + ['company']) move = fields.Many2One('account.move', 'Move', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ], depends=['company']) cancel_move = fields.Many2One('account.move', 'Cancel Move', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'invisible': ~Eval('cancel_move'), }, depends=['company']) account = fields.Many2One('account.account', 'Account', required=True, states=_states, depends=_depends + [ 'type', 'company', 'accounting_date', 'invoice_date'], domain=[ ('closed', '!=', True), ('company', '=', Eval('company', -1)), If(Eval('type') == 'out', ('type.receivable', '=', True), ('type.payable', '=', True)), ], context={ 'date': If(Eval('accounting_date'), Eval('accounting_date'), Eval('invoice_date')), }) payment_term = fields.Many2One('account.invoice.payment_term', 'Payment Term', states=_states, depends=_depends) lines = fields.One2Many('account.invoice.line', 'invoice', 'Lines', domain=[ ('company', '=', Eval('company', -1)), ('currency', '=', Eval('currency', -1)), ['OR', ('invoice_type', '=', Eval('type')), ('invoice_type', '=', None), ], ['OR', ('party', '=', Eval('party', -1)), ('party', '=', None), ], ], states={ 'readonly': ( (Eval('state') != 'draft') | ~Eval('company') | ~Eval('currency')), }, depends=['state', 'company', 'currency', 'type', 'party']) taxes = fields.One2Many('account.invoice.tax', 'invoice', 'Tax Lines', states=_states, depends=_depends) comment = fields.Text('Comment', states=_states, depends=_depends) origins = fields.Function(fields.Char('Origins'), 'get_origins') untaxed_amount = fields.Function(fields.Numeric('Untaxed', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount', searcher='search_untaxed_amount') tax_amount = fields.Function(fields.Numeric('Tax', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount', searcher='search_tax_amount') total_amount = fields.Function(fields.Numeric('Total', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount', searcher='search_total_amount') reconciled = fields.Function(fields.Date('Reconciled', states={ 'invisible': ~Eval('reconciled'), }), 'get_reconciled') lines_to_pay = fields.Function(fields.Many2Many( 'account.move.line', None, None, 'Lines to Pay'), 'get_lines_to_pay') payment_lines = fields.Many2Many('account.invoice-account.move.line', 'invoice', 'line', string='Payment Lines', domain=[ ('account', '=', Eval('account', -1)), ('party', 'in', [None, Eval('party', -1)]), ['OR', ('invoice_payment', '=', None), ('invoice_payment', '=', Eval('id', -1)), ], If(Eval('type') == 'out', If(Eval('total_amount', 0) >= 0, ('debit', '=', 0), ('credit', '=', 0)), If(Eval('total_amount', 0) >= 0, ('credit', '=', 0), ('debit', '=', 0))), ], states={ 'invisible': Eval('state') == 'paid', 'readonly': Eval('state') != 'posted', }, depends=['state', 'account', 'party', 'id', 'type', 'total_amount']) reconciliation_lines = fields.Function(fields.Many2Many( 'account.move.line', None, None, "Payment Lines", states={ 'invisible': Eval('state') != 'paid', }), 'get_reconciliation_lines') amount_to_pay_today = fields.Function(fields.Numeric('Amount to Pay Today', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount_to_pay') amount_to_pay = fields.Function(fields.Numeric('Amount to Pay', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), 'get_amount_to_pay') invoice_report_cache = fields.Binary('Invoice Report', readonly=True, file_id=file_id, store_prefix=store_prefix) invoice_report_cache_id = fields.Char('Invoice Report ID', readonly=True) invoice_report_format = fields.Char('Invoice Report Format', readonly=True) allow_cancel = fields.Function( fields.Boolean("Allow Cancel Invoice"), 'get_allow_cancel') del _states, _depends @classmethod def __setup__(cls): super(Invoice, cls).__setup__() cls._check_modify_exclude = { 'state', 'payment_lines', 'move', 'cancel_move', 'invoice_report_cache', 'invoice_report_format', 'lines'} cls._order = [ ('number', 'DESC NULLS FIRST'), ('id', 'DESC'), ] cls.journal.domain = [ If(Eval('type') == 'out', ('type', 'in', cls._journal_types('out')), ('type', 'in', cls._journal_types('in'))), ] cls.journal.depends += ['type'] cls.tax_identifier.domain = [ ('party', '=', Eval('company_party', -1)), ('type', 'in', cls.tax_identifier_types()), ] cls.tax_identifier.depends += ['company_party'] cls._transitions |= set(( ('draft', 'validated'), ('validated', 'posted'), ('draft', 'posted'), ('posted', 'posted'), ('posted', 'paid'), ('validated', 'draft'), ('paid', 'posted'), ('draft', 'cancelled'), ('validated', 'cancelled'), ('posted', 'cancelled'), ('cancelled', 'draft'), )) cls._buttons.update({ 'cancel': { 'invisible': ~Eval('allow_cancel', False), 'depends': ['allow_cancel'], }, 'draft': { 'invisible': ( ~Eval('state').in_(['cancelled', 'validated']) | ((Eval('state') == 'cancelled') & Eval('cancel_move', -1))), 'icon': If(Eval('state') == 'cancelled', 'tryton-undo', 'tryton-back'), 'depends': ['state'], }, 'validate_invoice': { 'pre_validate': ['OR', ('invoice_date', '!=', None), ('type', '!=', 'in'), ], 'invisible': Eval('state') != 'draft', 'depends': ['state'], }, 'post': { 'pre_validate': ['OR', ('invoice_date', '!=', None), ('type', '!=', 'in'), ], 'invisible': (~Eval('state').in_(['draft', 'validated']) | ((Eval('state') == 'posted') & Bool(Eval('move')))), 'depends': ['state', 'move'], }, 'pay': { 'invisible': Eval('state') != 'posted', 'depends': ['state'], }, 'process': { 'invisible': ~Eval('state').in_( ['posted', 'paid']), 'depends': ['state'], }, }) cls.__rpc__.update({ 'post': RPC( readonly=False, instantiate=0, fresh_session=True), }) @classmethod def __register__(cls, module_name): pool = Pool() Line = pool.get('account.invoice.line') Tax = pool.get('account.invoice.tax') sql_table = cls.__table__() line = Line.__table__() tax = Tax.__table__() super(Invoice, cls).__register__(module_name) transaction = Transaction() cursor = transaction.connection.cursor() table = cls.__table_handler__(module_name) # Migration from 3.8: remove invoice/credit note type cursor.execute(*sql_table.select(sql_table.id, where=sql_table.type.like('%_invoice') | sql_table.type.like('%_credit_note'), limit=1)) if cursor.fetchone(): for type_ in ['out', 'in']: cursor.execute(*sql_table.update( columns=[sql_table.type], values=[type_], where=sql_table.type == '%s_invoice' % type_)) cursor.execute(*line.update( columns=[line.invoice_type], values=[type_], where=line.invoice_type == '%s_invoice' % type_)) cursor.execute(*line.update( columns=[line.quantity, line.invoice_type], values=[-line.quantity, type_], where=(line.invoice_type == '%s_credit_note' % type_) & (line.invoice == Null) )) # Don't use UPDATE FROM because SQLite nor MySQL support it cursor.execute(*line.update( columns=[line.quantity, line.invoice_type], values=[-line.quantity, type_], where=line.invoice.in_(sql_table.select(sql_table.id, where=( sql_table.type == '%s_credit_note' % type_) )))) cursor.execute(*tax.update( columns=[tax.base, tax.amount, tax.base_sign], values=[-tax.base, -tax.amount, -tax.base_sign], where=tax.invoice.in_(sql_table.select(sql_table.id, where=( sql_table.type == '%s_credit_note' % type_) )))) cursor.execute(*sql_table.update( columns=[sql_table.type], values=[type_], where=sql_table.type == '%s_credit_note' % type_)) # Migration from 4.0: Drop not null on payment_term table.not_null_action('payment_term', 'remove') # Add index on create_date table.index_action('create_date', action='add') # Migration from 5.6: rename state cancel to cancelled cursor.execute(*sql_table.update( [sql_table.state], ['cancelled'], where=sql_table.state == 'cancel')) # Migration from 5.8: drop foreign key for sequence table.drop_fk('sequence') @staticmethod def default_type(): return Transaction().context.get('type', 'out') @staticmethod def default_state(): return 'draft' @staticmethod def default_currency(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.id @staticmethod def default_currency_digits(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.digits return 2 @staticmethod def default_company(): return Transaction().context.get('company') @fields.depends('company') def on_change_with_company_party(self, name=None): if self.company: return self.company.party.id @fields.depends('party', 'type', 'accounting_date', 'invoice_date') def on_change_with_account(self): account = None if self.party: with Transaction().set_context( date=self.accounting_date or self.invoice_date): if self.type == 'out': account = self.party.account_receivable_used elif self.type == 'in': account = self.party.account_payable_used return account.id if account else None @fields.depends('type') def on_change_type(self): Journal = Pool().get('account.journal') journal_type = { 'out': 'revenue', 'in': 'expense', }.get(self.type or 'out', 'revenue') journals = Journal.search([ ('type', '=', journal_type), ], limit=1) if journals: self.journal, = journals @classmethod def _journal_types(cls, invoice_type): if invoice_type == 'out': return ['revenue'] else: return ['expense'] @classmethod def order_accounting_date(cls, tables): table, _ = tables[None] return [Coalesce(table.accounting_date, table.invoice_date)] @fields.depends('party', 'type') def on_change_party(self): self.invoice_address = None if self.party: self.invoice_address = self.party.address_get(type='invoice') self.party_tax_identifier = self.party.tax_identifier if self.type == 'out': self.payment_term = self.party.customer_payment_term elif self.type == 'in': self.payment_term = self.party.supplier_payment_term @fields.depends('currency') def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 @fields.depends('invoice_date') def on_change_with_currency_date(self, name=None): Date = Pool().get('ir.date') return self.invoice_date or Date.today() @fields.depends('party') def on_change_with_party_lang(self, name=None): Config = Pool().get('ir.configuration') if self.party and self.party.lang: return self.party.lang.code return Config.get_language() @classmethod def get_type_name(cls, invoices, name): type_names = {} type2name = {} for type, name in cls.fields_get(fields_names=['type'] )['type']['selection']: type2name[type] = name for invoice in invoices: type_names[invoice.id] = type2name[invoice.type] return type_names @fields.depends(methods=['_on_change_lines_taxes']) def on_change_lines(self): self._on_change_lines_taxes() @fields.depends(methods=['_on_change_lines_taxes']) def on_change_taxes(self): self._on_change_lines_taxes() @fields.depends('lines', 'taxes', 'currency', 'accounting_date', 'invoice_date', # From tax_date methods=['_get_taxes']) def _on_change_lines_taxes(self): pool = Pool() InvoiceTax = pool.get('account.invoice.tax') self.untaxed_amount = Decimal('0.0') self.tax_amount = Decimal('0.0') self.total_amount = Decimal('0.0') computed_taxes = {} if self.lines: for line in self.lines: self.untaxed_amount += getattr(line, 'amount', None) or 0 computed_taxes = self._get_taxes() def is_zero(amount): if self.currency: return self.currency.is_zero(amount) else: return amount == Decimal('0.0') tax_keys = [] taxes = list(self.taxes or []) for tax in (self.taxes or []): if tax.manual: self.tax_amount += tax.amount or Decimal('0.0') continue key = tax._key if (key not in computed_taxes) or (key in tax_keys): taxes.remove(tax) continue tax_keys.append(key) if not is_zero(computed_taxes[key]['base'] - (tax.base or Decimal('0.0'))): self.tax_amount += computed_taxes[key]['amount'] tax.amount = computed_taxes[key]['amount'] tax.base = computed_taxes[key]['base'] else: self.tax_amount += tax.amount or Decimal('0.0') for key in computed_taxes: if key not in tax_keys: self.tax_amount += computed_taxes[key]['amount'] value = InvoiceTax.default_get( list(InvoiceTax._fields.keys()), with_rec_name=False) value.update(computed_taxes[key]) invoice_tax = InvoiceTax(**value) if invoice_tax.tax: invoice_tax.sequence = invoice_tax.tax.sequence taxes.append(invoice_tax) self.taxes = taxes if self.currency: self.untaxed_amount = self.currency.round(self.untaxed_amount) self.tax_amount = self.currency.round(self.tax_amount) self.total_amount = self.untaxed_amount + self.tax_amount if self.currency: self.total_amount = self.currency.round(self.total_amount) @classmethod def get_amount(cls, invoices, names): pool = Pool() InvoiceTax = pool.get('account.invoice.tax') Move = pool.get('account.move') MoveLine = pool.get('account.move.line') cursor = Transaction().connection.cursor() untaxed_amount = dict((i.id, Decimal(0)) for i in invoices) tax_amount = dict((i.id, Decimal(0)) for i in invoices) total_amount = dict((i.id, Decimal(0)) for i in invoices) type_name = cls.tax_amount._field.sql_type().base tax = InvoiceTax.__table__() to_round = False for sub_ids in grouped_slice(invoices): red_sql = reduce_ids(tax.invoice, sub_ids) cursor.execute(*tax.select(tax.invoice, Coalesce(Sum(tax.amount), 0).as_(type_name), where=red_sql, group_by=tax.invoice)) for invoice_id, sum_ in cursor: # SQLite uses float for SUM if not isinstance(sum_, Decimal): sum_ = Decimal(str(sum_)) to_round = True tax_amount[invoice_id] = sum_ # Float amount must be rounded to get the right precision if to_round: for invoice in invoices: tax_amount[invoice.id] = invoice.currency.round( tax_amount[invoice.id]) invoices_move = set() invoices_no_move = set() for invoice in invoices: if invoice.move: invoices_move.add(invoice.id) else: invoices_no_move.add(invoice.id) invoices_move = cls.browse(invoices_move) invoices_no_move = cls.browse(invoices_no_move) type_name = cls.total_amount._field.sql_type().base invoice = cls.__table__() move = Move.__table__() line = MoveLine.__table__() to_round = False for sub_ids in grouped_slice(invoices_move): red_sql = reduce_ids(invoice.id, sub_ids) cursor.execute(*invoice.join(move, condition=invoice.move == move.id ).join(line, condition=move.id == line.move ).select(invoice.id, Coalesce(Sum( Case(( line.second_currency == invoice.currency, line.amount_second_currency), else_=line.debit - line.credit)), 0).cast(type_name), where=(invoice.account == line.account) & red_sql, group_by=invoice.id)) for invoice_id, sum_ in cursor: # SQLite uses float for SUM if not isinstance(sum_, Decimal): sum_ = Decimal(str(sum_)) to_round = True total_amount[invoice_id] = sum_ for invoice in invoices_move: if invoice.type == 'in': total_amount[invoice.id] *= -1 # Float amount must be rounded to get the right precision if to_round: total_amount[invoice.id] = invoice.currency.round( total_amount[invoice.id]) untaxed_amount[invoice.id] = ( total_amount[invoice.id] - tax_amount[invoice.id]) for invoice in invoices_no_move: untaxed_amount[invoice.id] = sum( (line.amount for line in invoice.lines if line.type == 'line'), Decimal(0)) total_amount[invoice.id] = ( untaxed_amount[invoice.id] + tax_amount[invoice.id]) result = { 'untaxed_amount': untaxed_amount, 'tax_amount': tax_amount, 'total_amount': total_amount, } for key in list(result.keys()): if key not in names: del result[key] return result def get_reconciled(self, name): def get_reconciliation(line): if line.reconciliation and line.reconciliation.delegate_to: return get_reconciliation(line.reconciliation.delegate_to) else: return line.reconciliation reconciliations = list(map(get_reconciliation, self.lines_to_pay)) if not reconciliations: return None elif not all(reconciliations): return None else: return max(r.date for r in reconciliations) @classmethod def get_lines_to_pay(cls, invoices, name): pool = Pool() MoveLine = pool.get('account.move.line') line = MoveLine.__table__() invoice = cls.__table__() cursor = Transaction().connection.cursor() lines = defaultdict(list) for sub_ids in grouped_slice(invoices): red_sql = reduce_ids(invoice.id, sub_ids) cursor.execute(*invoice.join(line, condition=((invoice.move == line.move) & (invoice.account == line.account))).select( invoice.id, line.id, where=red_sql, order_by=(invoice.id, line.maturity_date.nulls_last))) for invoice_id, line_id in cursor: lines[invoice_id].append(line_id) return lines def get_reconciliation_lines(self, name): if self.state != 'paid': return lines = set() for line in self.move.lines: if line.account == self.account and line.reconciliation: for line in line.reconciliation.lines: if line not in self.move.lines: lines.add(line) return [l.id for l in sorted(lines, key=lambda l: l.date)] @classmethod def get_amount_to_pay(cls, invoices, name): pool = Pool() Currency = pool.get('currency.currency') Date = pool.get('ir.date') today = Date.today() res = dict((x.id, Decimal(0)) for x in invoices) for invoice in invoices: if invoice.state != 'posted': continue amount = Decimal(0) amount_currency = Decimal(0) for line in invoice.lines_to_pay: if line.reconciliation: continue if (name == 'amount_to_pay_today' and (not line.maturity_date or line.maturity_date > today)): continue if (line.second_currency and line.second_currency == invoice.currency): amount_currency += line.amount_second_currency else: amount += line.debit - line.credit for line in invoice.payment_lines: if line.reconciliation: continue if (line.second_currency and line.second_currency == invoice.currency): amount_currency += line.amount_second_currency else: amount += line.debit - line.credit if amount != Decimal(0): with Transaction().set_context(date=invoice.currency_date): amount_currency += Currency.compute( invoice.company.currency, amount, invoice.currency) if invoice.type == 'in' and amount_currency: amount_currency *= -1 res[invoice.id] = amount_currency return res @classmethod def search_total_amount(cls, name, clause): pool = Pool() Rule = pool.get('ir.rule') Line = pool.get('account.invoice.line') Tax = pool.get('account.invoice.tax') Invoice = pool.get('account.invoice') Currency = pool.get('currency.currency') type_name = cls.total_amount._field.sql_type().base line = Line.__table__() invoice = Invoice.__table__() currency = Currency.__table__() tax = Tax.__table__() _, operator, value = clause invoice_query = Rule.query_get('account.invoice') Operator = fields.SQL_OPERATORS[operator] # SQLite uses float for sum if backend.name == 'sqlite': value = float(value) union = (line.join(invoice, condition=(invoice.id == line.invoice) ).join(currency, condition=(currency.id == invoice.currency) ).select(line.invoice.as_('invoice'), Coalesce(Sum(Round((line.quantity * line.unit_price).cast( type_name), currency.digits)), 0).as_('total_amount'), where=line.invoice.in_(invoice_query), group_by=line.invoice) | tax.select(tax.invoice.as_('invoice'), Coalesce(Sum(tax.amount), 0).as_('total_amount'), where=tax.invoice.in_(invoice_query), group_by=tax.invoice)) query = union.select(union.invoice, group_by=union.invoice, having=Operator(Sum(union.total_amount).cast(type_name), value)) return [('id', 'in', query)] @classmethod def search_untaxed_amount(cls, name, clause): pool = Pool() Rule = pool.get('ir.rule') Line = pool.get('account.invoice.line') Invoice = pool.get('account.invoice') Currency = pool.get('currency.currency') type_name = cls.untaxed_amount._field.sql_type().base line = Line.__table__() invoice = Invoice.__table__() currency = Currency.__table__() _, operator, value = clause invoice_query = Rule.query_get('account.invoice') Operator = fields.SQL_OPERATORS[operator] # SQLite uses float for sum if backend.name == 'sqlite': value = float(value) query = line.join(invoice, condition=(invoice.id == line.invoice) ).join(currency, condition=(currency.id == invoice.currency) ).select(line.invoice, where=line.invoice.in_(invoice_query), group_by=line.invoice, having=Operator(Coalesce(Sum( Round((line.quantity * line.unit_price).cast( type_name), currency.digits)), 0).cast(type_name), value)) return [('id', 'in', query)] @classmethod def search_tax_amount(cls, name, clause): pool = Pool() Rule = pool.get('ir.rule') Tax = pool.get('account.invoice.tax') type_name = cls.tax_amount._field.sql_type().base tax = Tax.__table__() _, operator, value = clause invoice_query = Rule.query_get('account.invoice') Operator = fields.SQL_OPERATORS[operator] # SQLite uses float for sum if backend.name == 'sqlite': value = float(value) query = tax.select(tax.invoice, where=tax.invoice.in_(invoice_query), group_by=tax.invoice, having=Operator(Coalesce(Sum(tax.amount), 0).cast(type_name), value)) return [('id', 'in', query)] def get_allow_cancel(self, name): if self.state in {'draft', 'validated'}: return True if self.state == 'posted': return self.type == 'in' or self.company.cancel_invoice_out return False @property def taxable_lines(self): taxable_lines = [] for line in self.lines: if getattr(line, 'type', None) == 'line': taxable_lines.extend(line.taxable_lines) return taxable_lines @property def tax_date(self): return self.accounting_date or self.invoice_date @fields.depends('party', 'company') def _get_tax_context(self): context = {} if self.party and self.party.lang: context['language'] = self.party.lang.code if self.company: context['company'] = self.company.id return context def _compute_taxes(self): taxes = self._get_taxes() for tax in taxes.values(): tax['invoice'] = self.id return taxes @dualmethod def update_taxes(cls, invoices, exception=False): Tax = Pool().get('account.invoice.tax') to_create = [] to_delete = [] to_write = [] for invoice in invoices: if invoice.state in ('posted', 'paid', 'cancelled'): continue computed_taxes = invoice._compute_taxes() if not invoice.taxes: to_create.extend(computed_taxes.values()) else: tax_keys = [] for tax in invoice.taxes: if tax.manual: continue key = tax._key if (key not in computed_taxes) or (key in tax_keys): to_delete.append(tax) continue tax_keys.append(key) if not invoice.currency.is_zero( computed_taxes[key]['base'] - tax.base): to_write.extend(([tax], computed_taxes[key])) for key in computed_taxes: if key not in tax_keys: to_create.append(computed_taxes[key]) if exception and (to_create or to_delete or to_write): raise InvoiceTaxValidationError( gettext('account_invoice.msg_invoice_tax_invalid', invoice=invoice.rec_name)) if to_create: Tax.create(to_create) if to_delete: Tax.delete(to_delete) if to_write: Tax.write(*to_write) def _get_move_line(self, date, amount): ''' Return move line ''' pool = Pool() Currency = pool.get('currency.currency') MoveLine = pool.get('account.move.line') line = MoveLine() if self.currency != self.company.currency: with Transaction().set_context(date=self.currency_date): line.amount_second_currency = Currency.compute( self.company.currency, amount, self.currency) line.second_currency = self.currency else: line.amount_second_currency = None line.second_currency = None if amount <= 0: line.debit, line.credit = -amount, 0 else: line.debit, line.credit = 0, amount if line.amount_second_currency: line.amount_second_currency = ( line.amount_second_currency.copy_sign( line.debit - line.credit)) line.account = self.account if self.account.party_required: line.party = self.party line.maturity_date = date line.description = self.description return line def get_move(self): ''' Compute account move for the invoice and return the created move ''' pool = Pool() Move = pool.get('account.move') Period = pool.get('account.period') Date = pool.get('ir.date') Warning = pool.get('res.user.warning') Lang = pool.get('ir.lang') today = Date.today() if self.move: return self.move self.update_taxes(exception=True) move_lines = [] for line in self.lines: move_lines += line.get_move_lines() for tax in self.taxes: move_lines += tax.get_move_lines() total = Decimal('0.0') total_currency = Decimal('0.0') for line in move_lines: total += line.debit - line.credit if line.amount_second_currency: total_currency += line.amount_second_currency term_lines = [(self.payment_term_date or today, total)] if self.payment_term: payment_date = self.payment_term_date or self.invoice_date term_lines = self.payment_term.compute( total, self.company.currency, payment_date) remainder_total_currency = total_currency for date, amount in term_lines: if date < today: lang = Lang.get() warning_key = 'invoice_payment_term_%d' % self.id if Warning.check(warning_key): raise InvoicePaymentTermDateWarning(warning_key, gettext('account_invoice' '.msg_invoice_payment_term_date_past', invoice=self.rec_name, date=lang.strftime(date))) line = self._get_move_line(date, amount) if line.amount_second_currency: remainder_total_currency += line.amount_second_currency move_lines.append(line) if not self.currency.is_zero(remainder_total_currency): move_lines[-1].amount_second_currency -= \ remainder_total_currency accounting_date = self.accounting_date or self.invoice_date period_id = Period.find(self.company.id, date=accounting_date) move = Move() move.journal = self.journal move.period = period_id move.date = accounting_date move.origin = self move.company = self.company move.lines = move_lines return move @classmethod def set_number(cls, invoices): ''' Set number to the invoice ''' pool = Pool() Date = pool.get('ir.date') Lang = pool.get('ir.lang') Sequence = pool.get('ir.sequence.strict') today = Date.today() def accounting_date(invoice): return invoice.accounting_date or invoice.invoice_date or today invoices = sorted(invoices, key=accounting_date) sequences = set() for invoice in invoices: # Posted and paid invoices are tested by check_modify so we can # not modify tax_identifier nor number if invoice.state in {'posted', 'paid'}: continue if not invoice.tax_identifier: invoice.tax_identifier = invoice.get_tax_identifier() # Generated invoice may not fill the party tax identifier if not invoice.party_tax_identifier: invoice.party_tax_identifier = invoice.party.tax_identifier if invoice.number: continue if not invoice.invoice_date and invoice.type == 'out': invoice.invoice_date = today invoice.number, invoice.sequence = invoice.get_next_number() if invoice.type == 'out' and invoice.sequence not in sequences: date = accounting_date(invoice) # Do not need to lock the table # because sequence.get_id is sequential after_invoices = cls.search([ ('sequence', '=', invoice.sequence), ['OR', ('accounting_date', '>', date), [ ('accounting_date', '=', None), ('invoice_date', '>', date), ], ], ], limit=1, order=[('accounting_date', 'DESC')]) if after_invoices: after_invoice, = after_invoices raise InvoiceNumberError( gettext('account_invoice.msg_invoice_number_after', invoice=invoice.rec_name, sequence=Sequence(invoice.sequence).rec_name, date=Lang.get().strftime(date), after_invoice=after_invoice.rec_name)) sequences.add(invoice.sequence) cls.save(invoices) def get_next_number(self, pattern=None): "Return invoice number and sequence id used" pool = Pool() Period = pool.get('account.period') if pattern is None: pattern = {} else: pattern = pattern.copy() accounting_date = self.accounting_date or self.invoice_date period_id = Period.find( self.company.id, date=accounting_date, test_state=self.type != 'in') period = Period(period_id) fiscalyear = period.fiscalyear pattern.setdefault('company', self.company.id) pattern.setdefault('fiscalyear', fiscalyear.id) pattern.setdefault('period', period.id) for invoice_sequence in fiscalyear.invoice_sequences: if invoice_sequence.match(pattern): sequence = getattr( invoice_sequence, self._sequence_field) break else: raise InvoiceNumberError( gettext('account_invoice.msg_invoice_no_sequence', invoice=self.rec_name, fiscalyear=fiscalyear.rec_name)) with Transaction().set_context(date=accounting_date): return sequence.get(), sequence.id @property def _sequence_field(self): "Returns the field name of invoice_sequence to use" field = self.type if (all(l.amount <= 0 for l in self.lines if l.product) and self.total_amount < 0): field += '_credit_note' else: field += '_invoice' return field + '_sequence' @classmethod def tax_identifier_types(cls): return Pool().get('party.party').tax_identifier_types() def get_tax_identifier(self): "Return the default computed tax identifier" types = self.tax_identifier_types() for identifier in self.company.party.identifiers: if identifier.type in types: return identifier.id @property def is_modifiable(self): return not (self.state in {'posted', 'paid'} or (self.state == 'cancelled' and (self.move or self.cancel_move or self.number))) @classmethod def check_modify(cls, invoices): ''' Check if the invoices can be modified ''' for invoice in invoices: if not invoice.is_modifiable: raise AccessError( gettext('account_invoice.msg_invoice_modify', invoice=invoice.rec_name)) def get_rec_name(self, name): items = [] if self.number: items.append(self.number) if self.reference: items.append('[%s]' % self.reference) if not items: items.append('(%s)' % self.id) return ' '.join(items) @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [bool_op, ('number',) + tuple(clause[1:]), ('reference',) + tuple(clause[1:]), ('party',) + tuple(clause[1:]), ] def get_origins(self, name): return ', '.join(set(filter(None, (l.origin_name for l in self.lines)))) @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/form//field[@name="comment"]', 'spell', Eval('party_lang')), ('/tree', 'visual', If(( (Eval('type') == 'out') & (Eval('amount_to_pay_today', 0) > 0)) | ((Eval('type') == 'in') & (Eval('amount_to_pay_today', 0) < 0)), 'danger', If(Eval('state') == 'cancelled', 'muted', ''))), ] @classmethod def delete(cls, invoices): cls.check_modify(invoices) # Cancel before delete cls.cancel(invoices) for invoice in invoices: if invoice.state != 'cancelled': raise AccessError( gettext('account_invoice.msg_invoice_delete_cancel', invoice=invoice.rec_name)) if invoice.number: raise AccessError( gettext('account_invoice.msg_invoice_delete_numbered', invoice=invoice.rec_name)) super(Invoice, cls).delete(invoices) @classmethod def write(cls, *args): actions = iter(args) all_invoices = [] for invoices, values in zip(actions, actions): if set(values) - cls._check_modify_exclude: cls.check_modify(invoices) all_invoices += invoices update_tax = [i for i in all_invoices if i.state == 'draft'] super(Invoice, cls).write(*args) if update_tax: cls.update_taxes(update_tax) @classmethod def copy(cls, invoices, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('number', None) default.setdefault('sequence') default.setdefault('move', None) default.setdefault('cancel_move', None) default.setdefault('invoice_report_cache', None) default.setdefault('invoice_report_cache_id', None) default.setdefault('invoice_report_format', None) default.setdefault('payment_lines', None) default.setdefault('invoice_date', None) default.setdefault('accounting_date', None) default.setdefault('lines_to_pay', None) return super(Invoice, cls).copy(invoices, default=default) @classmethod def validate(cls, invoices): super(Invoice, cls).validate(invoices) for invoice in invoices: invoice.check_same_account() invoice.check_payment_lines() def check_same_account(self): for line in self.lines: if (line.type == 'line' and line.account == self.account): raise InvoiceValidationError( gettext('account_invoice.msg_invoice_same_account_line', account=self.account.rec_name, invoice=self.rec_name, line=line.rec_name)) def check_payment_lines(self): amount = sum(l.debit - l.credit for l in self.lines_to_pay) payment_amount = sum(l.debit - l.credit for l in self.payment_lines) if abs(amount) < abs(payment_amount): raise InvoiceValidationError( gettext('account_invoice' '.msg_invoice_payment_lines_greater_amount', invoice=self.rec_name)) def get_reconcile_lines_for_amount(self, amount): ''' Return list of lines and the remainder to make reconciliation. ''' Result = namedtuple('Result', ['lines', 'remainder']) lines = [l for l in self.payment_lines + self.lines_to_pay if not l.reconciliation] best = Result([], self.total_amount) for n in range(len(lines), 0, -1): for comb_lines in combinations(lines, n): remainder = sum((l.debit - l.credit) for l in comb_lines) remainder -= amount result = Result(list(comb_lines), remainder) if self.currency.is_zero(remainder): return result if abs(remainder) < abs(best.remainder): best = result return best def pay_invoice(self, amount, payment_method, date, description=None, amount_second_currency=None, second_currency=None, overpayment=0): ''' Adds a payment of amount to an invoice using the journal, date and description. If overpayment is set, then only the amount minus the overpayment is used to pay off the invoice. Returns the payment lines. ''' pool = Pool() Move = pool.get('account.move') Line = pool.get('account.move.line') Period = pool.get('account.period') pay_line = Line(account=self.account) counterpart_line = Line() lines = [pay_line, counterpart_line] pay_amount = amount - overpayment if pay_amount >= 0: if self.type == 'out': pay_line.debit, pay_line.credit = 0, pay_amount else: pay_line.debit, pay_line.credit = pay_amount, 0 else: if self.type == 'out': pay_line.debit, pay_line.credit = -pay_amount, 0 else: pay_line.debit, pay_line.credit = 0, -pay_amount if overpayment: overpayment_line = Line(account=self.account) lines.insert(1, overpayment_line) overpayment_line.debit = ( abs(overpayment) if pay_line.debit else 0) overpayment_line.credit = ( abs(overpayment) if pay_line.credit else 0) counterpart_line.debit = abs(amount) if pay_line.credit else 0 counterpart_line.credit = abs(amount) if pay_line.debit else 0 if counterpart_line.debit: payment_acccount = 'debit_account' else: payment_acccount = 'credit_account' counterpart_line.account = getattr( payment_method, payment_acccount).current(date=date) for line in lines: if line.account.party_required: line.party = self.party if amount_second_currency: line.amount_second_currency = amount_second_currency.copy_sign( line.debit - line.credit) line.second_currency = second_currency period_id = Period.find(self.company.id, date=date) move = Move( journal=payment_method.journal, period=period_id, date=date, origin=self, description=description, company=self.company, lines=lines) move.save() Move.post([move]) payment_lines = [l for l in move.lines if l.account == self.account] payment_line = [l for l in payment_lines if (l.debit, l.credit) == (pay_line.debit, pay_line.credit)][0] self.add_payment_lines({self: [payment_line]}) return payment_lines @classmethod def add_payment_lines(cls, payments): "Add value lines to the key invoice from the payment dictionary." to_write = [] for invoice, lines in payments.items(): if invoice.state == 'paid': raise AccessError( gettext('account_invoice' '.msg_invoice_payment_lines_add_remove_paid', invoice=invoice.rec_name)) to_write.append([invoice]) to_write.append({'payment_lines': [('add', lines)]}) if to_write: cls.write(*to_write) @classmethod def remove_payment_lines(cls, lines): "Remove payment lines from their invoices." pool = Pool() PaymentLine = pool.get('account.invoice-account.move.line') payments = defaultdict(list) ids = list(map(int, lines)) for sub_ids in grouped_slice(ids): payment_lines = PaymentLine.search([ ('line', 'in', list(sub_ids)), ]) for payment_line in payment_lines: payments[payment_line.invoice].append(payment_line.line) to_write = [] for invoice, lines in payments.items(): if invoice.state == 'paid': raise AccessError( gettext('account_invoice' '.msg_invoice_payment_lines_add_remove_paid', invoice=invoice.rec_name)) to_write.append([invoice]) to_write.append({'payment_lines': [('remove', lines)]}) if to_write: cls.write(*to_write) def print_invoice(self): ''' Generate invoice report and store it in invoice_report field. ''' if self.invoice_report_cache: return InvoiceReport = Pool().get('account.invoice', type='report') InvoiceReport.execute([self.id], {}) def _credit(self, **values): ''' Return values to credit invoice. ''' credit = self.__class__(**values) for field in ('company', 'party', 'invoice_address', 'currency', 'journal', 'account', 'payment_term', 'description', 'comment', 'type'): setattr(credit, field, getattr(self, field)) credit.lines = [line._credit() for line in self.lines] credit.taxes = [tax._credit() for tax in self.taxes if tax.manual] return credit @classmethod def credit(cls, invoices, refund=False, **values): ''' Credit invoices and return ids of new invoices. Return the list of new invoice ''' new_invoices = [i._credit(**values) for i in invoices] cls.save(new_invoices) cls.update_taxes(new_invoices) if refund: cls.post(new_invoices) for invoice, new_invoice in zip(invoices, new_invoices): if invoice.state != 'posted': raise AccessError( gettext('account_invoice' '.msg_invoice_credit_refund_not_posted', invoice=invoice.rec_name)) invoice.cancel_move = new_invoice.move cls.save(invoices) cls.cancel(invoices) return new_invoices @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, invoices): Move = Pool().get('account.move') moves = [] for invoice in invoices: if invoice.move: moves.append(invoice.move) if moves: Move.delete(moves) @classmethod @ModelView.button @Workflow.transition('validated') def validate_invoice(cls, invoices): pool = Pool() Move = pool.get('account.move') invoices_in = cls.browse([i for i in invoices if i.type == 'in']) cls.set_number(invoices_in) moves = [] for invoice in invoices_in: move = invoice.get_move() if move != invoice.move: invoice.move = move moves.append(move) if moves: Move.save(moves) cls.save(invoices_in) @classmethod @Workflow.transition('posted') def post_batch(cls, invoices): pool = Pool() Date = pool.get('ir.date') today = Date.today() cls.set_number(invoices) for invoice in invoices: if not invoice.payment_term_date: invoice.payment_term_date = today cls.save(invoices) with Transaction().set_context(_skip_warnings=True): cls.__queue__._post(invoices) @classmethod @ModelView.button @Workflow.transition('posted') def post(cls, invoices): cls._post(invoices) @classmethod def _post(cls, invoices): pool = Pool() Move = pool.get('account.move') cls.set_number(invoices) moves = [] for invoice in invoices: move = invoice.get_move() if move != invoice.move: invoice.move = move moves.append(move) if invoice.state != 'posted': invoice.state = 'posted' if moves: Move.save(moves) cls.save(invoices) Move.post([i.move for i in invoices if i.move.state != 'posted']) reconciled = [] for invoice in invoices: if invoice.type == 'out': invoice.print_invoice() if invoice.reconciled: reconciled.append(invoice) if reconciled: cls.__queue__.process(reconciled) @classmethod @ModelView.button_action('account_invoice.wizard_pay') def pay(cls, invoices): pass @classmethod @ModelView.button def process(cls, invoices): paid = [] posted = [] for invoice in invoices: if invoice.state not in ('posted', 'paid'): continue if invoice.reconciled: paid.append(invoice) else: posted.append(invoice) cls.paid(paid) cls.post(posted) @classmethod @Workflow.transition('paid') def paid(cls, invoices): # Remove links to lines which actually do not pay the invoice cls._clean_payments(invoices) @classmethod @ModelView.button @Workflow.transition('cancelled') def cancel(cls, invoices): pool = Pool() Move = pool.get('account.move') Line = pool.get('account.move.line') cancel_moves = [] delete_moves = [] to_save = [] for invoice in invoices: if invoice.move or invoice.number: if invoice.move.state == 'draft': delete_moves.append(invoice.move) elif not invoice.cancel_move: if (invoice.type == 'out' and not invoice.company.cancel_invoice_out): raise AccessError( gettext('account_invoice' '.msg_invoice_customer_cancel_move', invoice=invoice.rec_name)) invoice.cancel_move = invoice.move.cancel() to_save.append(invoice) cancel_moves.append(invoice.cancel_move) if cancel_moves: Move.save(cancel_moves) cls.save(to_save) if delete_moves: Move.delete(delete_moves) if cancel_moves: Move.post(cancel_moves) # Write state before reconcile to prevent invoice to go to paid state cls.write(invoices, { 'state': 'cancelled', }) for invoice in invoices: if not invoice.move or not invoice.cancel_move: continue to_reconcile = [] for line in invoice.move.lines + invoice.cancel_move.lines: if line.account == invoice.account: to_reconcile.append(line) Line.reconcile(to_reconcile) cls._clean_payments(invoices) @classmethod def _clean_payments(cls, invoices): to_write = [] for invoice in invoices: to_remove = [] reconciliations = [l.reconciliation for l in invoice.lines_to_pay] for payment_line in invoice.payment_lines: if payment_line.reconciliation not in reconciliations: to_remove.append(payment_line.id) if to_remove: to_write.append([invoice]) to_write.append({ 'payment_lines': [('remove', to_remove)], }) if to_write: cls.write(*to_write) class InvoicePaymentLine(ModelSQL): 'Invoice - Payment Line' __name__ = 'account.invoice-account.move.line' invoice = fields.Many2One('account.invoice', 'Invoice', ondelete='CASCADE', select=True, required=True) invoice_account = fields.Function( fields.Many2One('account.account', "Invoice Account"), 'get_invoice') invoice_party = fields.Function( fields.Many2One('party.party', "Invoice Party"), 'get_invoice') line = fields.Many2One( 'account.move.line', 'Payment Line', ondelete='CASCADE', select=True, required=True, domain=[ ('account', '=', Eval('invoice_account')), ('party', '=', Eval('invoice_party')), ], depends=['invoice_account', 'invoice_party']) @classmethod def __setup__(cls): super(InvoicePaymentLine, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ ('line_unique', Unique(t, t.line), 'account_invoice.msg_invoice_payment_line_unique'), ] @classmethod def get_invoice(cls, records, names): result = {} for name in names: result[name] = {} invoice_account = 'invoice_account' in result invoice_party = 'invoice_party' in result for record in records: if invoice_account: result['invoice_account'][record.id] = ( record.invoice.account.id) if invoice_party: if record.invoice.account.party_required: party = record.invoice.party.id else: party = None result['invoice_party'][record.id] = party return result class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin): 'Invoice Line' __name__ = 'account.invoice.line' _states = { 'readonly': Eval('invoice_state') != 'draft', } _depends = ['invoice_state'] invoice = fields.Many2One('account.invoice', 'Invoice', ondelete='CASCADE', select=True, states={ 'required': (~Eval('invoice_type') & Eval('party') & Eval('currency') & Eval('company')), 'invisible': Bool(Eval('context', {}).get('standalone')), 'readonly': _states['readonly'] & Bool(Eval('invoice')), }, depends=['invoice_type', 'party', 'company', 'currency'] + _depends) invoice_state = fields.Function( fields.Selection('get_invoice_states', "Invoice State"), 'on_change_with_invoice_state') invoice_type = fields.Selection('get_invoice_types', "Invoice Type", select=True, states={ 'readonly': Eval('context', {}).get('type') | Eval('type'), 'required': ~Eval('invoice'), }, depends=['invoice', 'type']) party = fields.Many2One('party.party', 'Party', select=True, states={ 'required': ~Eval('invoice'), 'readonly': _states['readonly'], }, context={ 'company': Eval('company', -1), }, depends=['invoice', 'company'] + _depends) party_lang = fields.Function(fields.Char('Party Language'), 'on_change_with_party_lang') currency = fields.Many2One( 'currency.currency', "Currency", required=True, states=_states, depends=_depends) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') company = fields.Many2One( 'company.company', "Company", required=True, select=True, states=_states, depends=_depends) type = fields.Selection([ ('line', 'Line'), ('subtotal', 'Subtotal'), ('title', 'Title'), ('comment', 'Comment'), ], 'Type', select=True, required=True, states={ 'invisible': Bool(Eval('context', {}).get('standalone')), 'readonly': _states['readonly'], }, depends=_depends) quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), states={ 'invisible': Eval('type') != 'line', 'required': Eval('type') == 'line', 'readonly': _states['readonly'], }, depends=['type', 'unit_digits'] + _depends) unit = fields.Many2One('product.uom', 'Unit', ondelete='RESTRICT', states={ 'required': Bool(Eval('product')), 'invisible': Eval('type') != 'line', 'readonly': _states['readonly'], }, domain=[ If(Bool(Eval('product_uom_category')), ('category', '=', Eval('product_uom_category')), ('category', '!=', -1)), ], depends=['product', 'type', 'product_uom_category'] + _depends) unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') product = fields.Many2One('product.product', 'Product', ondelete='RESTRICT', domain=[ If(Bool(Eval('product_uom_category')), ('default_uom_category', '=', Eval('product_uom_category')), ()), ], states={ 'invisible': Eval('type') != 'line', 'readonly': _states['readonly'], }, context={ 'company': Eval('company', None), }, depends=['type', 'product_uom_category', 'company'] + _depends) product_uom_category = fields.Function( fields.Many2One('product.uom.category', 'Product Uom Category'), 'on_change_with_product_uom_category') account = fields.Many2One('account.account', 'Account', ondelete='RESTRICT', states={ 'invisible': Eval('type') != 'line', 'required': Eval('type') == 'line', 'readonly': _states['readonly'], }, context={ 'date': If(Eval('_parent_invoice', {}).get('accounting_date'), Eval('_parent_invoice', {}).get('accounting_date'), Eval('_parent_invoice', {}).get('invoice_date')), }, depends=['type', 'invoice_type', 'company'] + _depends) unit_price = fields.Numeric('Unit Price', digits=price_digits, states={ 'invisible': Eval('type') != 'line', 'required': Eval('type') == 'line', 'readonly': _states['readonly'], }, depends=['type'] + _depends) amount = fields.Function(fields.Numeric('Amount', digits=(16, Eval('_parent_invoice', {}).get('currency_digits', Eval('currency_digits', 2))), states={ 'invisible': ~Eval('type').in_(['line', 'subtotal']), }, depends=['type', 'currency_digits']), 'get_amount') description = fields.Text('Description', size=None, states=_states, depends=_depends) summary = fields.Function(fields.Char('Summary'), 'on_change_with_summary') note = fields.Text('Note') taxes = fields.Many2Many('account.invoice.line-account.tax', 'line', 'tax', 'Taxes', order=[('tax.sequence', 'ASC'), ('tax.id', 'ASC')], domain=[('parent', '=', None), ['OR', ('group', '=', None), ('group.kind', 'in', If(Bool(Eval('_parent_invoice')), If(Eval('_parent_invoice', {}).get('type') == 'out', ['sale', 'both'], ['purchase', 'both']), If(Eval('invoice_type') == 'out', ['sale', 'both'], ['purchase', 'both'])) )], ('company', '=', Eval('company', -1)), ], states={ 'invisible': Eval('type') != 'line', 'readonly': _states['readonly'] | ~Bool(Eval('account')), }, depends=['type', 'invoice_type', 'company', 'account'] + _depends) taxes_deductible_rate = fields.Numeric( "Taxes Deductible Rate", digits=(14, 10), domain=[ ('taxes_deductible_rate', '>=', 0), ('taxes_deductible_rate', '<=', 1), ], states={ 'invisible': Eval('invoice_type') != 'in', }, depends=['invoice_type']) taxes_date = fields.Date( "Taxes Date", states={ 'invisible': Eval('type') != 'line', 'readonly': _states['readonly'], }, depends=['type'] + _depends, help="The date at which the taxes are computed.\n" "Leave empty for the accounting date.") invoice_taxes = fields.Function(fields.Many2Many('account.invoice.tax', None, None, 'Invoice Taxes'), 'get_invoice_taxes') origin = fields.Reference('Origin', selection='get_origin', select=True, states=_states, depends=_depends) del _states, _depends @classmethod def __setup__(cls): super(InvoiceLine, cls).__setup__() cls._check_modify_exclude = {'note', 'origin'} # Set account domain dynamically for kind cls.account.domain = [ ('closed', '!=', True), ('company', '=', Eval('company', -1)), If(Bool(Eval('_parent_invoice')), If(Eval('_parent_invoice', {}).get('type') == 'out', cls._account_domain('out'), If(Eval('_parent_invoice', {}).get('type') == 'in', cls._account_domain('in'), ['OR', cls._account_domain('out'), cls._account_domain('in')])), If(Eval('invoice_type') == 'out', cls._account_domain('out'), If(Eval('invoice_type') == 'in', cls._account_domain('in'), ['OR', cls._account_domain('out'), cls._account_domain('in')]))), ] cls.account.depends += ['company', 'invoice_type'] cls.sequence.states.update({ 'invisible': Bool(Eval('context', {}).get('standalone')), }) @staticmethod def _account_domain(type_): if type_ == 'out': return ['OR', ('type.revenue', '=', True)] elif type_ == 'in': return ['OR', ('type.expense', '=', True), ('type.debt', '=', True), ] @classmethod def __register__(cls, module_name): pool = Pool() Invoice = pool.get('account.invoice') invoice = Invoice.__table__() sql_table = cls.__table__() transaction = Transaction() cursor = transaction.connection.cursor() update = transaction.connection.cursor() super(InvoiceLine, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 3.4: company is required cursor.execute(*sql_table.join(invoice, condition=sql_table.invoice == invoice.id ).select(sql_table.id, invoice.company, where=sql_table.company == Null)) for line_id, company_id in cursor: update.execute(*sql_table.update([sql_table.company], [company_id], where=sql_table.id == line_id)) # Migration from 4.6: drop required on description table.not_null_action('description', action='remove') # Migration from 5.0: remove check constraints table.drop_constraint('type_account') table.drop_constraint('type_invoice') @classmethod def get_invoice_types(cls): pool = Pool() Invoice = pool.get('account.invoice') return Invoice.fields_get(['type'])['type']['selection'] + [(None, '')] @fields.depends( 'invoice', '_parent_invoice.currency', '_parent_invoice.company', '_parent_invoice.type', methods=['on_change_company']) def on_change_invoice(self): if self.invoice: self.currency = self.invoice.currency self.company = self.invoice.company self.on_change_company() self.invoice_type = self.invoice.type @fields.depends('company', 'invoice', '_parent_invoice.type', 'invoice_type') def on_change_company(self): invoice_type = self.invoice.type if self.invoice else self.invoice_type if (invoice_type == 'in' and self.company and self.company.purchase_taxes_expense): self.taxes_deductible_rate = 0 @staticmethod def default_currency(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.id @staticmethod def default_currency_digits(): Company = Pool().get('company.company') if Transaction().context.get('company'): company = Company(Transaction().context['company']) return company.currency.digits return 2 @staticmethod def default_unit_digits(): return 2 @staticmethod def default_company(): return Transaction().context.get('company') @staticmethod def default_type(): return 'line' @classmethod def default_invoice_state(cls): return 'draft' @classmethod def get_invoice_states(cls): pool = Pool() Invoice = pool.get('account.invoice') return Invoice.fields_get(['state'])['state']['selection'] @fields.depends('invoice', '_parent_invoice.state') def on_change_with_invoice_state(self, name=None): if self.invoice: return self.invoice.state return 'draft' @fields.depends('invoice', '_parent_invoice.party', 'party') def on_change_with_party_lang(self, name=None): Config = Pool().get('ir.configuration') if self.invoice and self.invoice.party: party = self.invoice.party else: party = self.party if party and party.lang: return party.lang.code return Config.get_language() @fields.depends('description') def on_change_with_summary(self, name=None): return firstline(self.description or '') @fields.depends('unit') def on_change_with_unit_digits(self, name=None): if self.unit: return self.unit.digits return 2 @fields.depends('currency') def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 @fields.depends( 'type', 'quantity', 'unit_price', 'taxes_deductible_rate', 'invoice', '_parent_invoice.currency', 'currency', 'taxes', '_parent_invoice.type', 'invoice_type', methods=['_get_taxes']) def on_change_with_amount(self): if self.type == 'line': currency = (self.invoice.currency if self.invoice else self.currency) amount = (Decimal(str(self.quantity or '0.0')) * (self.unit_price or Decimal('0.0'))) invoice_type = ( self.invoice.type if self.invoice else self.invoice_type) if invoice_type == 'in' and self.taxes_deductible_rate != 1: with Transaction().set_context(_deductible_rate=1): tax_amount = sum( t['amount'] for t in self._get_taxes().values()) non_deductible_amount = ( tax_amount * (1 - self.taxes_deductible_rate)) amount += non_deductible_amount if currency: return currency.round(amount) return amount return Decimal('0.0') def get_amount(self, name): if self.type == 'line': return self.on_change_with_amount() elif self.type == 'subtotal': subtotal = Decimal(0) for line2 in self.invoice.lines: if line2.type == 'line': subtotal += line2.on_change_with_amount() elif line2.type == 'subtotal': if self == line2: break subtotal = Decimal(0) return subtotal else: return Decimal(0) @property def origin_name(self): if isinstance(self.origin, self.__class__): return self.origin.invoice.rec_name return self.origin.rec_name if self.origin else None @classmethod def default_taxes_deductible_rate(cls): return 1 @property def taxable_lines(self): # In case we're called from an on_change we have to use some sensible # defaults context = Transaction().context if (getattr(self, 'invoice', None) and getattr(self.invoice, 'type', None)): invoice_type = self.invoice.type else: invoice_type = getattr(self, 'invoice_type', None) if invoice_type == 'in': if context.get('_deductible_rate') is not None: deductible_rate = context['_deductible_rate'] else: deductible_rate = getattr(self, 'taxes_deductible_rate', 1) if not deductible_rate: return [] else: deductible_rate = 1 return [( getattr(self, 'taxes', None) or [], ((getattr(self, 'unit_price', None) or Decimal(0)) * deductible_rate), getattr(self, 'quantity', None) or 0, getattr(self, 'tax_date', None), )] @property def tax_date(self): if getattr(self, 'taxes_date', None): return self.taxes_date elif hasattr(self, 'invoice') and hasattr(self.invoice, 'tax_date'): return self.invoice.tax_date def _get_tax_context(self): if self.invoice: return self.invoice._get_tax_context() else: context = {} if self.company: context['company'] = self.company.id return context def get_invoice_taxes(self, name): if not self.invoice: return taxes_keys = list(self._get_taxes().keys()) taxes = [] for tax in self.invoice.taxes: if tax.manual: continue key = tax._key if key in taxes_keys: taxes.append(tax.id) return taxes @fields.depends('invoice', '_parent_invoice.accounting_date', '_parent_invoice.invoice_date') def _get_tax_rule_pattern(self): ''' Get tax rule pattern ''' if self.invoice: date = self.invoice.accounting_date or self.invoice.invoice_date else: date = None return { 'date': date, } @fields.depends('product', 'unit', '_parent_invoice.type', '_parent_invoice.party', 'party', 'invoice', 'invoice_type', '_parent_invoice.invoice_date', '_parent_invoice.accounting_date', 'company', methods=['_get_tax_rule_pattern']) def on_change_product(self): if not self.product: return party = None if self.invoice and self.invoice.party: party = self.invoice.party elif self.party: party = self.party date = (self.invoice.accounting_date or self.invoice.invoice_date if self.invoice else None) if self.invoice and self.invoice.type: type_ = self.invoice.type else: type_ = self.invoice_type if type_ == 'in': with Transaction().set_context(date=date): self.account = self.product.account_expense_used taxes = [] pattern = self._get_tax_rule_pattern() for tax in self.product.supplier_taxes_used: if party and party.supplier_tax_rule: tax_ids = party.supplier_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax) if party and party.supplier_tax_rule: tax_ids = party.supplier_tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) self.taxes = taxes if self.company and self.company.purchase_taxes_expense: self.taxes_deductible_rate = 0 else: self.taxes_deductible_rate = ( self.product.supplier_taxes_deductible_rate_used) else: with Transaction().set_context(date=date): self.account = self.product.account_revenue_used taxes = [] pattern = self._get_tax_rule_pattern() for tax in self.product.customer_taxes_used: if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax.id) if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) self.taxes = taxes category = self.product.default_uom.category if not self.unit or self.unit.category != category: self.unit = self.product.default_uom.id self.unit_digits = self.product.default_uom.digits @fields.depends('product') def on_change_with_product_uom_category(self, name=None): if self.product: return self.product.default_uom_category.id @fields.depends('account', 'product', 'invoice', '_parent_invoice.party', '_parent_invoice.type', 'party', 'invoice', 'invoice_type', methods=['_get_tax_rule_pattern']) def on_change_account(self): if self.product: return taxes = [] party = None if self.invoice and self.invoice.party: party = self.invoice.party elif self.party: party = self.party if self.invoice and self.invoice.type: type_ = self.invoice.type else: type_ = self.invoice_type if party and type_: if type_ == 'in': tax_rule = party.supplier_tax_rule else: tax_rule = party.customer_tax_rule else: tax_rule = None if self.account: pattern = self._get_tax_rule_pattern() for tax in self.account.taxes: if tax_rule: tax_ids = tax_rule.apply(tax, pattern) if tax_ids: taxes.extend(tax_ids) continue taxes.append(tax) if tax_rule: tax_ids = tax_rule.apply(None, pattern) if tax_ids: taxes.extend(tax_ids) self.taxes = taxes @classmethod def _get_origin(cls): 'Return list of Model names for origin Reference' return [cls.__name__] @classmethod def get_origin(cls): Model = Pool().get('ir.model') get_name = Model.get_name models = cls._get_origin() return [(None, '')] + [(m, get_name(m)) for m in models] def get_rec_name(self, name): pool = Pool() Lang = pool.get('ir.lang') if self.product: lang = Lang.get() prefix = (lang.format( '%.*f', (self.unit.digits, self.quantity or 0)) + '%s %s' % (self.unit.symbol, self.product.rec_name)) elif self.account: prefix = self.account.rec_name else: prefix = '(%s)' % self.id if self.invoice: return '%s @ %s' % (prefix, self.invoice.rec_name) else: return prefix @classmethod def search_rec_name(cls, name, clause): return ['OR', ('invoice.rec_name',) + tuple(clause[1:]), ('product.rec_name',) + tuple(clause[1:]), ('account.rec_name',) + tuple(clause[1:]), ] @classmethod def check_modify(cls, lines, fields=None): ''' Check if the lines can be modified ''' if fields is None or fields - cls._check_modify_exclude: for line in lines: if line.invoice and not line.invoice.is_modifiable: raise AccessError( gettext('account_invoice.msg_invoice_line_modify', line=line.rec_name, invoice=line.invoice.rec_name)) @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/form//field[@name="note"]|/form//field[@name="description"]', 'spell', Eval('party_lang'))] @classmethod def delete(cls, lines): cls.check_modify(lines) super(InvoiceLine, cls).delete(lines) @classmethod def write(cls, *args): actions = iter(args) for lines, values in zip(actions, actions): cls.check_modify(lines, set(values)) super(InvoiceLine, cls).write(*args) @classmethod def create(cls, vlist): Invoice = Pool().get('account.invoice') invoice_ids = [] for vals in vlist: if vals.get('invoice'): invoice_ids.append(vals.get('invoice')) for invoice in Invoice.browse(invoice_ids): if invoice.state in ('posted', 'paid', 'cancelled'): raise AccessError( gettext('account_invoice.msg_invoice_line_create', invoice=invoice.rec_name)) return super(InvoiceLine, cls).create(vlist) @classmethod def copy(cls, lines, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('origin', None) return super(InvoiceLine, cls).copy(lines, default=default) @classmethod def validate(cls, lines): super(InvoiceLine, cls).validate(lines) for line in lines: line.check_same_account() def check_same_account(self): if self.type == 'line': if (self.invoice and self.account == self.invoice.account): raise InvoiceLineValidationError( gettext('account_invoice.msg_invoice_same_account_line', account=self.account.rec_name, invoice=self.invoice.rec_name, line=self.rec_name)) def _compute_taxes(self): pool = Pool() Currency = pool.get('currency.currency') TaxLine = pool.get('account.tax.line') tax_lines = [] if self.type != 'line': return tax_lines taxes = self._get_taxes().values() for tax in taxes: amount = tax['base'] with Transaction().set_context( date=self.invoice.currency_date): amount = Currency.compute( self.invoice.currency, amount, self.invoice.company.currency) tax_line = TaxLine() tax_line.amount = amount tax_line.type = 'base' tax_line.tax = tax['tax'] tax_lines.append(tax_line) return tax_lines def get_move_lines(self): ''' Return a list of move lines instances for invoice line ''' pool = Pool() Currency = pool.get('currency.currency') MoveLine = pool.get('account.move.line') if self.type != 'line': return [] line = MoveLine() if self.invoice.currency != self.invoice.company.currency: with Transaction().set_context(date=self.invoice.currency_date): amount = Currency.compute(self.invoice.currency, self.amount, self.invoice.company.currency) line.amount_second_currency = self.amount line.second_currency = self.invoice.currency else: amount = self.amount line.amount_second_currency = None line.second_currency = None if amount >= 0: if self.invoice.type == 'out': line.debit, line.credit = 0, amount else: line.debit, line.credit = amount, 0 else: if self.invoice.type == 'out': line.debit, line.credit = -amount, 0 else: line.debit, line.credit = 0, -amount if line.amount_second_currency: line.amount_second_currency = ( line.amount_second_currency.copy_sign( line.debit - line.credit)) line.account = self.account if self.account.party_required: line.party = self.invoice.party line.origin = self line.tax_lines = self._compute_taxes() return [line] def _credit(self): ''' Return credit line. ''' line = self.__class__() line.origin = self if self.quantity: line.quantity = -self.quantity else: line.quantity = self.quantity for field in ('sequence', 'type', 'invoice_type', 'unit_price', 'description', 'unit', 'product', 'account'): setattr(line, field, getattr(self, field)) line.taxes_date = self.tax_date line.taxes = self.taxes return line class InvoiceLineTax(ModelSQL): 'Invoice Line - Tax' __name__ = 'account.invoice.line-account.tax' _table = 'account_invoice_line_account_tax' line = fields.Many2One('account.invoice.line', 'Invoice Line', ondelete='CASCADE', select=True, required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', required=True) class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): 'Invoice Tax' __name__ = 'account.invoice.tax' _rec_name = 'description' _states = { 'readonly': Eval('invoice_state') != 'draft', } _depends = ['invoice_state'] invoice = fields.Many2One('account.invoice', 'Invoice', ondelete='CASCADE', select=True, required=True, states={ 'readonly': _states['readonly'] & Bool(Eval('invoice')), }, depends=_depends) invoice_state = fields.Function( fields.Selection('get_invoice_states', "Invoice State"), 'on_change_with_invoice_state') description = fields.Char('Description', size=None, required=True, states=_states, depends=_depends) sequence_number = fields.Function(fields.Integer('Sequence Number'), 'get_sequence_number') account = fields.Many2One('account.account', 'Account', required=True, domain=[ ('type', '!=', None), ('closed', '!=', True), ('company', '=', Eval('_parent_invoice', {}).get('company', 0)), ], states=_states, depends=_depends) base = fields.Numeric('Base', required=True, digits=(16, Eval('_parent_invoice', {}).get('currency_digits', 2)), states=_states, depends=_depends) amount = fields.Numeric('Amount', required=True, digits=(16, Eval('_parent_invoice', {}).get('currency_digits', 2)), states=_states, depends=['tax', 'base', 'manual'] + _depends) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') manual = fields.Boolean('Manual', states=_states, depends=_depends) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', domain=[ ('company', '=', Eval('_parent_invoice', {}).get('company', 0)), ], states={ 'readonly': ~Eval('manual', False) | _states['readonly'], }, depends=['manual'] + _depends) legal_notice = fields.Text("Legal Notice", states=_states, depends=_depends) del _states, _depends @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('invoice') @classmethod def __register__(cls, module_name): super(InvoiceTax, cls).__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 4.6: drop base_sign and tax_sign table.not_null_action('base_sign', action='remove') table.not_null_action('tax_sign', action='remove') @staticmethod def default_base(): return Decimal('0.0') @staticmethod def default_amount(): return Decimal('0.0') @staticmethod def default_manual(): return True @classmethod def default_invoice_state(cls): pool = Pool() Invoice = pool.get('account.invoice') return Invoice.default_state() @classmethod def get_invoice_states(cls): pool = Pool() Invoice = pool.get('account.invoice') return Invoice.fields_get(['state'])['state']['selection'] @fields.depends('invoice', '_parent_invoice.state') def on_change_with_invoice_state(self, name=None): if self.invoice: return self.invoice.state @fields.depends('invoice', '_parent_invoice.currency') def on_change_with_currency(self, name=None): if self.invoice: return self.invoice.currency.id @fields.depends('tax', 'invoice', '_parent_invoice.party', 'base') def on_change_tax(self): Tax = Pool().get('account.tax') if not self.tax: return if self.invoice: context = self.invoice._get_tax_context() else: context = {} with Transaction().set_context(**context): tax = Tax(self.tax.id) self.description = tax.description if self.base >= 0: self.account = tax.invoice_account else: self.account = tax.credit_note_account @fields.depends('tax', 'base', 'amount', 'manual', 'invoice', '_parent_invoice.currency') def on_change_with_amount(self): Tax = Pool().get('account.tax') if self.tax and self.manual: tax = self.tax base = self.base or Decimal(0) for values in Tax.compute([tax], base, 1): if (values['tax'] == tax and values['base'] == base): amount = values['amount'] if self.invoice.currency: amount = self.invoice.currency.round(amount) return amount return self.amount @property def _key(self): # Same as _TaxKey tax_id = self.tax.id if self.tax else -1 return (self.account.id, tax_id, self.base >= 0) @classmethod def check_modify(cls, taxes): ''' Check if the taxes can be modified ''' for tax in taxes: if not tax.invoice.is_modifiable: raise AccessError( gettext('account_invoice.msg_invoice_tax_modify', tax=tax.rec_name, invoice=tax.invoice.rec_name)) def get_sequence_number(self, name): i = 1 for tax in self.invoice.taxes: if tax == self: return i i += 1 return 0 @classmethod def delete(cls, taxes): cls.check_modify(taxes) super(InvoiceTax, cls).delete(taxes) @classmethod def write(cls, *args): taxes = sum(args[0::2], []) cls.check_modify(taxes) super(InvoiceTax, cls).write(*args) @classmethod def create(cls, vlist): Invoice = Pool().get('account.invoice') invoice_ids = [] for vals in vlist: if vals.get('invoice'): invoice_ids.append(vals['invoice']) for invoice in Invoice.browse(invoice_ids): if invoice.state in ('posted', 'paid', 'cancelled'): raise AccessError( gettext('account_invoice.msg_invoice_tax_create', invoice=invoice.rec_name)) return super(InvoiceTax, cls).create(vlist) def get_move_lines(self): ''' Return a list of move lines instances for invoice tax ''' Currency = Pool().get('currency.currency') pool = Pool() Currency = pool.get('currency.currency') MoveLine = pool.get('account.move.line') TaxLine = pool.get('account.tax.line') line = MoveLine() if not self.amount: return [] line.description = self.description if self.invoice.currency != self.invoice.company.currency: with Transaction().set_context(date=self.invoice.currency_date): amount = Currency.compute(self.invoice.currency, self.amount, self.invoice.company.currency) line.amount_second_currency = self.amount line.second_currency = self.invoice.currency else: amount = self.amount line.amount_second_currency = None line.second_currency = None if amount >= 0: if self.invoice.type == 'out': line.debit, line.credit = 0, amount else: line.debit, line.credit = amount, 0 else: if self.invoice.type == 'out': line.debit, line.credit = -amount, 0 else: line.debit, line.credit = 0, -amount if line.amount_second_currency: line.amount_second_currency = ( line.amount_second_currency.copy_sign( line.debit - line.credit)) line.account = self.account if self.account.party_required: line.party = self.invoice.party line.origin = self if self.tax: tax_line = TaxLine() tax_line.amount = amount tax_line.type = 'tax' tax_line.tax = self.tax line.tax_lines = [tax_line] return [line] def _credit(self): ''' Return credit tax. ''' line = self.__class__() line.base = -self.base line.amount = -self.amount for field in ['description', 'sequence', 'manual', 'account', 'tax']: setattr(line, field, getattr(self, field)) return line class PaymentMethod(DeactivableMixin, ModelSQL, ModelView): 'Payment Method' __name__ = 'account.invoice.payment.method' company = fields.Many2One('company.company', "Company", required=True) name = fields.Char("Name", required=True, translate=True) journal = fields.Many2One( 'account.journal', "Journal", required=True, domain=[('type', '=', 'cash')], context={ 'company': Eval('company', -1), }, depends=['company']) credit_account = fields.Many2One('account.account', "Credit Account", required=True, domain=[ ('type', '!=', None), ('closed', '!=', True), ('company', '=', Eval('company')), ], depends=['company']) debit_account = fields.Many2One('account.account', "Debit Account", required=True, domain=[ ('type', '!=', None), ('closed', '!=', True), ('company', '=', Eval('company')), ], depends=['company']) @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def default_company(cls): return Transaction().context.get('company') class InvoiceReport(Report): __name__ = 'account.invoice' @classmethod def __setup__(cls): super(InvoiceReport, cls).__setup__() cls.__rpc__['execute'] = RPC(False) @classmethod def _execute(cls, records, header, data, action): pool = Pool() Invoice = pool.get('account.invoice') # Re-instantiate because records are TranslateModel invoice, = Invoice.browse(records) if invoice.invoice_report_cache: return ( invoice.invoice_report_format, bytes(invoice.invoice_report_cache)) else: result = super()._execute(records, header, data, action) # If the invoice is posted or paid and the report not saved in # invoice_report_cache there was an error somewhere. So we save it # now in invoice_report_cache if invoice.state in {'posted', 'paid'} and invoice.type == 'out': format_, data = result invoice.invoice_report_format = format_ invoice.invoice_report_cache = \ Invoice.invoice_report_cache.cast(data) invoice.save() return result @classmethod def render(cls, *args, **kwargs): # Reset to default language to always have header and footer rendered # in the default language with Transaction().set_context(language=False): return super().render(*args, **kwargs) @classmethod def execute(cls, ids, data): with Transaction().set_context(address_with_party=True): return super().execute(ids, data) @classmethod def get_context(cls, records, header, data): pool = Pool() Date = pool.get('ir.date') context = super().get_context(records, header, data) context['invoice'] = context['record'] context['today'] = Date.today() return context class PayInvoiceStart(ModelView): 'Pay Invoice' __name__ = 'account.invoice.pay.start' amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)), depends=['currency_digits'], required=True) currency = fields.Many2One('currency.currency', 'Currency', required=True) currency_digits = fields.Integer('Currency Digits', readonly=True) description = fields.Char('Description', size=None) company = fields.Many2One('company.company', "Company", readonly=True) invoice_account = fields.Many2One( 'account.account', "Invoice Account", readonly=True) payment_method = fields.Many2One( 'account.invoice.payment.method', "Payment Method", required=True, domain=[ ('company', '=', Eval('company')), ('debit_account', '!=', Eval('invoice_account')), ('credit_account', '!=', Eval('invoice_account')), ], depends=['company', 'amount', 'invoice_account']) date = fields.Date('Date', required=True) @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() @staticmethod def default_currency_digits(): return 2 @fields.depends('currency') def on_change_with_currency_digits(self): if self.currency: return self.currency.digits return 2 class PayInvoiceAsk(ModelView): 'Pay Invoice' __name__ = 'account.invoice.pay.ask' type = fields.Selection([ ('writeoff', "Write-Off"), ('partial', "Partial Payment"), ('overpayment', "Overpayment"), ], 'Type', required=True) writeoff = fields.Many2One( 'account.move.reconcile.write_off', "Write Off", domain=[ ('company', '=', Eval('company')), ], states={ 'invisible': Eval('type') != 'writeoff', 'required': Eval('type') == 'writeoff', }, depends=['company', 'type']) amount = fields.Numeric('Payment Amount', digits=(16, Eval('currency_digits', 2)), readonly=True, depends=['currency_digits']) currency = fields.Many2One('currency.currency', 'Payment Currency', readonly=True) currency_digits = fields.Integer('Payment Currency Digits', readonly=True) amount_writeoff = fields.Numeric('Write-Off Amount', digits=(16, Eval('currency_digits_writeoff', 2)), readonly=True, depends=['currency_digits_writeoff', 'type'], states={ 'invisible': Eval('type') != 'writeoff', }) currency_writeoff = fields.Many2One('currency.currency', 'Write-Off Currency', readonly=True, states={ 'invisible': Eval('type') != 'writeoff', }, depends=['type']) currency_digits_writeoff = fields.Integer('Write-Off Currency Digits', required=True, readonly=True) lines_to_pay = fields.Many2Many('account.move.line', None, None, 'Lines to Pay', readonly=True) lines = fields.Many2Many('account.move.line', None, None, 'Lines', domain=[ ('id', 'in', Eval('lines_to_pay')), ('reconciliation', '=', None), ], states={ 'invisible': Eval('type') != 'writeoff', }, depends=['lines_to_pay', 'type']) payment_lines = fields.Many2Many('account.move.line', None, None, 'Payment Lines', readonly=True, states={ 'invisible': Eval('type') != 'writeoff', }, depends=['type']) company = fields.Many2One('company.company', 'Company', readonly=True) invoice = fields.Many2One('account.invoice', 'Invoice', readonly=True) date = fields.Date('Date', readonly=True) @staticmethod def default_type(): return 'partial' @fields.depends('lines', 'amount', 'currency', 'currency_writeoff', 'invoice', 'payment_lines', 'date') def on_change_lines(self): Currency = Pool().get('currency.currency') if self.currency and self.currency_writeoff: with Transaction().set_context(date=self.date): amount = Currency.compute(self.currency, self.amount, self.currency_writeoff) else: amount = self.amount self.amount_writeoff = Decimal('0.0') if not self.invoice: return for line in self.lines: self.amount_writeoff += line.debit - line.credit for line in self.payment_lines: self.amount_writeoff += line.debit - line.credit if self.invoice.type == 'in': self.amount_writeoff = - self.amount_writeoff - amount else: self.amount_writeoff = self.amount_writeoff - amount class PayInvoice(Wizard): 'Pay Invoice' __name__ = 'account.invoice.pay' start = StateView('account.invoice.pay.start', 'account_invoice.pay_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('OK', 'choice', 'tryton-ok', default=True), ]) choice = StateTransition() ask = StateView('account.invoice.pay.ask', 'account_invoice.pay_ask_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('OK', 'pay', 'tryton-ok', default=True), ]) pay = StateTransition() @classmethod def __setup__(cls): super(PayInvoice, cls).__setup__() cls.__rpc__['create'].fresh_session = True def get_reconcile_lines_for_amount(self, invoice, amount): if invoice.type == 'in': amount *= -1 return invoice.get_reconcile_lines_for_amount(amount) def default_start(self, fields): default = {} invoice = self.record default['company'] = invoice.company.id default['currency'] = invoice.currency.id default['currency_digits'] = invoice.currency.digits default['amount'] = (invoice.amount_to_pay_today or invoice.amount_to_pay) default['invoice_account'] = invoice.account.id return default def transition_choice(self): pool = Pool() Currency = pool.get('currency.currency') invoice = self.record with Transaction().set_context(date=self.start.date): amount = Currency.compute(self.start.currency, self.start.amount, invoice.company.currency) amount_invoice = Currency.compute( self.start.currency, self.start.amount, invoice.currency) _, remainder = self.get_reconcile_lines_for_amount(invoice, amount) if (remainder == Decimal('0.0') and amount_invoice <= invoice.amount_to_pay): return 'pay' return 'ask' def default_ask(self, fields): pool = Pool() Currency = pool.get('currency.currency') default = {} invoice = self.record default['lines_to_pay'] = [x.id for x in invoice.lines_to_pay if not x.reconciliation] default['amount'] = self.start.amount default['date'] = self.start.date default['currency'] = self.start.currency.id default['currency_digits'] = self.start.currency_digits default['company'] = invoice.company.id with Transaction().set_context(date=self.start.date): amount = Currency.compute(self.start.currency, self.start.amount, invoice.company.currency) amount_invoice = Currency.compute( self.start.currency, self.start.amount, invoice.currency) if invoice.company.currency.is_zero(amount): lines = invoice.lines_to_pay else: lines, _ = self.get_reconcile_lines_for_amount(invoice, amount) default['lines'] = [x.id for x in lines] for line_id in default['lines'][:]: if line_id not in default['lines_to_pay']: default['lines'].remove(line_id) default['payment_lines'] = [x.id for x in invoice.payment_lines if not x.reconciliation] default['currency_writeoff'] = invoice.company.currency.id default['currency_digits_writeoff'] = invoice.company.currency.digits default['invoice'] = invoice.id if (amount_invoice > invoice.amount_to_pay or invoice.company.currency.is_zero(amount)): default['type'] = 'writeoff' return default def transition_pay(self): pool = Pool() Currency = pool.get('currency.currency') MoveLine = pool.get('account.move.line') Lang = pool.get('ir.lang') invoice = self.record with Transaction().set_context(date=self.start.date): amount = Currency.compute(self.start.currency, self.start.amount, invoice.company.currency) amount_invoice = Currency.compute( self.start.currency, self.start.amount, invoice.currency) reconcile_lines, remainder = \ self.get_reconcile_lines_for_amount(invoice, amount) amount_second_currency = None second_currency = None if self.start.currency != invoice.company.currency: amount_second_currency = self.start.amount second_currency = self.start.currency overpayment = 0 if (0 <= invoice.amount_to_pay < amount_invoice or amount_invoice < invoice.amount_to_pay <= 0): if self.ask.type == 'partial': lang = Lang.get() raise PayInvoiceError( gettext('account_invoice' '.msg_invoice_pay_amount_greater_amount_to_pay', invoice=invoice.rec_name, amount_to_pay=lang.currency( invoice.amount_to_pay, invoice.currency))) else: overpayment = amount_invoice - invoice.amount_to_pay lines = [] if not invoice.company.currency.is_zero(amount): lines = invoice.pay_invoice(amount, self.start.payment_method, self.start.date, self.start.description, amount_second_currency, second_currency, overpayment) if remainder: if self.ask.type != 'partial': to_reconcile = {l for l in self.ask.lines} to_reconcile.update( l for l in invoice.payment_lines if not l.reconciliation) if self.ask.type == 'writeoff': to_reconcile.update(lines) if to_reconcile: MoveLine.reconcile( to_reconcile, writeoff=self.ask.writeoff, date=self.start.date) else: reconcile_lines += lines if reconcile_lines: MoveLine.reconcile(reconcile_lines) return 'end' class CreditInvoiceStart(ModelView): 'Credit Invoice' __name__ = 'account.invoice.credit.start' invoice_date = fields.Date("Invoice Date") with_refund = fields.Boolean('With Refund', states={ 'readonly': ~Eval('with_refund_allowed'), 'invisible': ~Eval('with_refund_allowed'), }, depends=['with_refund_allowed'], help='If true, the current invoice(s) will be cancelled.') with_refund_allowed = fields.Boolean("With Refund Allowed", readonly=True) class CreditInvoice(Wizard): 'Credit Invoice' __name__ = 'account.invoice.credit' start = StateView('account.invoice.credit.start', 'account_invoice.credit_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Credit', 'credit', 'tryton-ok', default=True), ]) credit = StateAction('account_invoice.act_invoice_form') def default_start(self, fields): default = { 'with_refund': True, 'with_refund_allowed': True, } for invoice in self.records: if invoice.state != 'posted' or invoice.type == 'in': default['with_refund'] = False default['with_refund_allowed'] = False break if invoice.payment_lines: default['with_refund'] = False return default @property def _credit_options(self): return dict( refund=self.start.with_refund, invoice_date=self.start.invoice_date, ) def do_credit(self, action): credit_invoices = self.model.credit( self.records, **self._credit_options) data = {'res_id': [i.id for i in credit_invoices]} if len(credit_invoices) == 1: action['views'].reverse() return action, data