# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime as dt import re from collections import defaultdict from decimal import Decimal from itertools import chain, groupby import stdnum.exceptions from genshi.template.text import TextTemplate from sql import Null from sql.aggregate import Sum from sql.conditionals import Coalesce from sql.functions import CharLength, Round from sql.operators import Exists from stdnum import iso7064, iso11649 from trytond import backend, config from trytond.i18n import gettext from trytond.model import ( ChatMixin, DeactivableMixin, Index, ModelSQL, ModelView, Unique, Workflow, dualmethod, fields, sequence_ordered) from trytond.model.exceptions import AccessError from trytond.modules.account.exceptions import AccountMissing from trytond.modules.account.tax import TaxableMixin from trytond.modules.company.model import ( employee_field, reset_employee, set_employee) from trytond.modules.currency.fields import Monetary from trytond.modules.product import price_digits from trytond.pool import Pool from trytond.pyson import Bool, Eval, Id, If from trytond.report import Report from trytond.rpc import RPC from trytond.tools import ( cached_property, firstline, grouped_slice, reduce_ids, slugify, sqlite_apply_types) from trytond.transaction import Transaction from trytond.wizard import ( Button, StateAction, StateReport, StateTransition, StateView, Wizard) from .exceptions import ( InvoiceFutureWarning, InvoiceNumberError, InvoicePaymentTermDateWarning, InvoiceSimilarWarning, InvoiceTaxesWarning, InvoiceTaxValidationError, InvoiceValidationError, PayInvoiceError) 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 InvoiceReportMixin: __slots__ = () 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) class Invoice( Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin, ChatMixin): __name__ = 'account.invoice' _rec_name = 'number' _order_name = 'number' _states = { 'readonly': Eval('state') != 'draft', } company = fields.Many2One( 'company.company', "Company", required=True, states={ 'readonly': ( _states['readonly'] | Eval('party', True) | Eval('lines', [0])), }, context={ 'party_contact_mechanism_usage': 'invoice', }) company_party = fields.Function( fields.Many2One( 'party.party', "Company Party", context={ 'company': Eval('company', -1), 'party_contact_mechanism_usage': 'invoice', }, depends={'company'}), 'on_change_with_company_party') tax_identifier = fields.Many2One( 'party.identifier', "Tax Identifier", ondelete='RESTRICT', states=_states) type = fields.Selection([ ('out', "Customer"), ('in', "Supplier"), ], "Type", required=True, states={ 'readonly': ((Eval('state') != 'draft') | Eval('context', {}).get('type') | (Eval('lines', [0]) & Eval('type'))), }) type_name = fields.Function(fields.Char('Type'), 'get_type_name') number = fields.Char("Number", readonly=True) number_alnum = fields.Char("Number Alphanumeric", readonly=True) number_digit = fields.Integer("Number Digit", readonly=True) number_digit._sql_type = 'BIGINT' reference = fields.Char( "Reference", states={ 'readonly': ( Eval('has_report_cache', False) & ~Id('account', 'group_account_admin').in_( Eval('context', {}).get('groups', []))), }) description = fields.Char("Description", size=None, states={ 'readonly': ( (Eval('state') != 'draft') & ~Id('account', 'group_account_admin').in_( Eval('context', {}).get('groups', []))), }) validated_by = employee_field( "Validated By", states=['validated', 'posted', 'paid', 'cancelled']) posted_by = employee_field( "Posted By", states=['posted', 'paid', 'cancelled']) state = fields.Selection([ ('draft', "Draft"), ('validated', "Validated"), ('posted', "Posted"), ('paid', "Paid"), ('cancelled', "Cancelled"), ], "State", readonly=True, sort=False) invoice_date = fields.Date('Invoice Date', states={ 'readonly': Eval('state').in_( If(Eval('type') == 'in', ['validated', 'posted', 'paid'], ['posted', 'paid'])), 'required': Eval('state').in_( If(Eval('type') == 'in', ['validated', 'posted', 'paid'], ['posted', 'paid'])), }) accounting_date = fields.Date('Accounting Date', states=_states) payment_term_date = fields.Date( "Payment Term Date", states=_states, help="The date from which the payment term is calculated.\n" "Leave empty to use the invoice date.") supplier_payment_reference_type = fields.Selection([ (None, ""), ('creditor_reference', "Creditor Reference"), ], "Payment Reference Type", states={ 'invisible': Eval('type', 'out') != 'in', }) supplier_payment_reference_type_string = ( supplier_payment_reference_type.translated( 'supplier_payment_reference_type')) supplier_payment_reference = fields.Char( "Payment Reference", states={ 'invisible': Eval('type', 'out') != 'in', }) customer_payment_reference = fields.Function(fields.Char( "Customer Payment Reference", states={ 'invisible': Eval('type', 'out') != 'out', }), 'get_customer_payment_reference', searcher='search_customer_payment_reference') sequence = fields.Integer("Sequence", readonly=True) sequence_type_cache = fields.Selection([ (None, ""), ('invoice', "Invoice"), ('credit_note', "Credit Note"), ], "Sequence Type Cache", readonly=True) party = fields.Many2One( 'party.party', 'Party', required=True, states=_states, context={ 'company': Eval('company', -1), 'party_contact_mechanism_usage': 'invoice', }, depends={'company'}) party_tax_identifier = fields.Many2One( 'party.identifier', "Party Tax Identifier", ondelete='RESTRICT', states=_states) 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, domain=[('party', '=', Eval('party', -1))]) currency = fields.Many2One('currency.currency', 'Currency', required=True, states={ 'readonly': ( _states['readonly'] | (Eval('lines', [0]) & Eval('currency'))), }) currency_date = fields.Function(fields.Date('Currency Date'), 'on_change_with_currency_date') journal = fields.Many2One( 'account.journal', 'Journal', states={ 'readonly': _states['readonly'], 'required': Eval('state') != 'draft', }, context={ 'company': Eval('company', -1), }, depends={'company'}) move = fields.Many2One('account.move', 'Move', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ]) additional_moves = fields.Many2Many( 'account.invoice-additional-account.move', 'invoice', 'move', "Additional Moves", readonly=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'invisible': ~Eval('additional_moves'), }) cancel_move = fields.Many2One('account.move', 'Cancel Move', readonly=True, domain=[ ('company', '=', Eval('company', -1)), ], states={ 'invisible': ~Eval('cancel_move'), }) account = fields.Many2One('account.account', 'Account', required=True, states=_states, 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", ondelete='RESTRICT', states=_states) alternative_payees = fields.Many2Many( 'account.invoice.alternative_payee', 'invoice', 'party', "Alternative Payee", states=_states, size=If(~Eval('move'), 1, None), context={ 'company': Eval('company', -1), }, depends=['company']) lines = fields.One2Many('account.invoice.line', 'invoice', 'Lines', domain=[ ('company', '=', Eval('company', -1)), ('currency', '=', Eval('currency', -1)), ['OR', ('account', '=', None), ('account', '!=', Eval('account', -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') | ~Eval('account')), }) line_lines = fields.One2Many( 'account.invoice.line', 'invoice', "Line - Lines", readonly=True, filter=[ ('type', '=', 'line'), ]) taxes = fields.One2Many( 'account.invoice.tax', 'invoice', 'Tax Lines', domain=[ ('account', '!=', Eval('account', -1)), ], states={ 'readonly': ( (Eval('state') != 'draft') | ~Eval('account')), }) comment = fields.Text("Comment", states={ 'readonly': ( (Eval('state') != 'draft') & ~Id('account', 'group_account_admin').in_( Eval('context', {}).get('groups', []))), }) origins = fields.Function(fields.Char('Origins'), 'get_origins') untaxed_amount = fields.Function(Monetary( "Untaxed", currency='currency', digits='currency'), 'get_amount', searcher='search_untaxed_amount') untaxed_amount_cache = fields.Numeric( "Untaxed Cache", digits='currency', readonly=True) tax_amount = fields.Function(Monetary( "Tax", currency='currency', digits='currency'), 'get_amount', searcher='search_tax_amount') tax_amount_cache = fields.Numeric( "Tax Cache", digits='currency', readonly=True) total_amount = fields.Function(Monetary( "Total", currency='currency', digits='currency'), 'get_amount', searcher='search_total_amount') total_amount_cache = fields.Numeric( "Total Cache", digits='currency', readonly=True) 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)), ['OR', ('currency', '=', Eval('currency', -1)), ('second_currency', '=', Eval('currency', -1)), ], ['OR', ('party', 'in', [None, Eval('party', -1)]), ('party', 'in', Eval('alternative_payees', [])), ], ['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', }) reconciliation_lines = fields.Function(fields.Many2Many( 'account.move.line', None, None, "Payment Lines", states={ 'invisible': ( ~Eval('state').in_(['paid', 'cancelled']) | ~Eval('reconciliation_lines')), }), 'get_reconciliation_lines') amount_to_pay_today = fields.Function(Monetary( "Amount to Pay Today", currency='currency', digits='currency'), 'get_amount_to_pay') amount_to_pay = fields.Function(Monetary( "Amount to Pay", currency='currency', digits='currency'), 'get_amount_to_pay') invoice_report_revisions = fields.One2Many( 'account.invoice.report.revision', 'invoice', "Invoice Report Revisions", readonly=True, states={ 'invisible': ~Eval('invoice_report_revisions'), }) allow_cancel = fields.Function( fields.Boolean("Allow Cancel Invoice"), 'get_allow_cancel') has_payment_method = fields.Function( fields.Boolean("Has Payment Method"), 'get_has_payment_method') has_report_cache = fields.Function( fields.Boolean("Has Report Cached"), 'get_has_report_cache') has_account_move = fields.Function( fields.Boolean("Has Account Move"), 'on_change_with_has_account_move') del _states @classmethod def __setup__(cls): pool = Pool() Party = pool.get('party.party') cls.number.search_unaccented = False cls.reference.search_unaccented = False super().__setup__() t = cls.__table__() cls._sql_indexes.update({ Index(t, (t.reference, Index.Similarity())), Index( t, (t.state, Index.Equality(cardinality='low')), where=t.state.in_(['draft', 'validated', 'posted'])), Index(t, (t.total_amount_cache, Index.Range())), Index( t, (t.total_amount_cache, Index.Equality(cardinality='low')), include=[t.id], where=t.total_amount_cache == Null), Index(t, (t.untaxed_amount_cache, Index.Range())), Index( t, (t.untaxed_amount_cache, Index.Equality(cardinality='low')), include=[t.id], where=t.untaxed_amount_cache == Null), Index(t, (t.tax_amount_cache, Index.Range())), Index( t, (t.tax_amount_cache, Index.Equality(cardinality='low')), include=[t.id], where=t.tax_amount_cache == Null), }) cls._check_modify_exclude = { 'state', 'alternative_payees', 'payment_lines', 'move', 'cancel_move', 'additional_moves', 'description', 'invoice_report_cache', 'invoice_report_format', 'comment', 'total_amount_cache', 'tax_amount_cache', 'untaxed_amount_cache', 'lines', 'reference', 'invoice_report_cache_id', 'invoice_report_revisions'} 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'))), ] tax_identifier_types = Party.tax_identifier_types() cls.tax_identifier.domain = [ ('party', '=', Eval('company_party', -1)), ('type', 'in', tax_identifier_types), ] cls.party_tax_identifier.domain = [ ('party', '=', Eval('party', -1)), ('type', 'in', tax_identifier_types), ] 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') & ~Eval('has_account_move', False))), 'depends': ['state', 'has_account_move'], }, 'pay': { 'invisible': ( (Eval('state') != 'posted') | ~Eval('has_payment_method', False)), 'depends': ['state', 'has_payment_method'], }, 'reschedule_lines_to_pay': { 'invisible': ( ~Eval('lines_to_pay') | Eval('reconciled', False)), 'depends': ['lines_to_pay', 'reconciled'], }, 'delegate_lines_to_pay': { 'invisible': ( ~Eval('lines_to_pay') | Eval('reconciled', False)), 'depends': ['lines_to_pay', 'reconciled'], }, '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): super().__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 6.6: drop not null on journal table.not_null_action('journal', 'remove') @classmethod def order_number(cls, tables): table, _ = tables[None] return [ ~((table.state == 'cancelled') & (table.number == Null)), CharLength(table.number), table.number] @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_company(): return Transaction().context.get('company') @fields.depends( 'company', 'tax_identifier', methods=['on_change_with_company_party']) def on_change_company(self): company_party = self.on_change_with_company_party() if self.company: if self.tax_identifier: if self.tax_identifier.party != company_party: self.tax_identifier = None else: self.tax_identifier = None @fields.depends('company') def on_change_with_company_party(self, name=None): return self.company.party if self.company else None @fields.depends(methods=['set_journal', 'on_change_party']) def on_change_type(self): self.set_journal() self.on_change_party() @classmethod def _journal_types(cls, invoice_type): if invoice_type == 'out': return ['revenue'] else: return ['expense'] @fields.depends('type') def set_journal(self, pattern=None): pool = Pool() Journal = pool.get('account.journal') pattern = pattern.copy() if pattern is not None else {} pattern.setdefault('type', { 'out': 'revenue', 'in': 'expense', }.get(self.type)) self.journal = Journal.find(pattern) @classmethod def order_accounting_date(cls, tables): table, _ = tables[None] return [Coalesce(table.accounting_date, table.invoice_date)] @fields.depends('party', 'type', methods=['_update_account']) def on_change_party(self): 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.account = self.party.account_receivable_used self.payment_term = self.party.customer_payment_term elif self.type == 'in': self.account = self.party.account_payable_used self.payment_term = self.party.supplier_payment_term else: self.invoice_address = None self.account = None self.payment_term = None self.party_tax_identifier = None self._update_account() @fields.depends(methods=['_update_account']) def on_change_accounting_date(self): self._update_account() @fields.depends(methods=['_update_account']) def on_change_invoice_date(self): self._update_account() @fields.depends('account', 'accounting_date', 'invoice_date') def _update_account(self): "Update account to current account" if self.account: account = self.account.current( date=self.accounting_date or self.invoice_date) if account != self.account: self.account = account @fields.depends('invoice_date', 'company') def on_change_with_currency_date(self, name=None): Date = Pool().get('ir.date') if self.company: company_id = self.company.id else: company_id = Transaction().context.get('company') with Transaction().set_context(company=company_id): return self.invoice_date or Date.today() @fields.depends( 'supplier_payment_reference_type', 'supplier_payment_reference') def on_change_with_supplier_payment_reference(self): reference = self.supplier_payment_reference if reference and (type := self.supplier_payment_reference_type): reference = getattr( self, f'_format_supplier_payment_reference_{type}')(reference) return reference @classmethod def _format_supplier_payment_reference_creditor_reference(cls, reference): try: return iso11649.format(reference) except stdnum.exceptions.ValidationError: return reference def get_customer_payment_reference(self, name): return self.format_customer_payment_reference( self._customer_payment_reference_type()) @classmethod def search_customer_payment_reference(cls, name, clause): _, operator, value = clause[:3] if operator == '=': return ['OR', *cls._search_customer_payment_reference(value)] else: return [] @classmethod def _search_customer_payment_reference(cls, value): if iso11649.is_valid(value): yield ('number_alnum', '=', value[4:]) def _customer_payment_reference_type(self): return 'creditor_reference' def _customer_payment_reference_number_alnum(self, source): if source == 'invoice': return self.number_alnum elif source == 'party': return self.party.code_alnum def _customer_payment_reference_number_digit(self, source): if source == 'invoice': return self.number_digit elif source == 'party': return self.party.code_digit def format_customer_payment_reference(self, type): pool = Pool() Confifugaration = pool.get('account.configuration') configuration = Confifugaration(1) source = configuration.get_multivalue( 'customer_payment_reference_number', company=self.company.id) return getattr( self, f'_format_customer_payment_reference_{type}')(source) def _format_customer_payment_reference_creditor_reference(self, source): number = self._customer_payment_reference_number_alnum(source) if number: number = number[-25:].upper() check_digits = iso7064.mod_97_10.calc_check_digits(f'{number}RF') return iso11649.format(f'RF{check_digits}{number}') @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() @fields.depends('move') def on_change_with_has_account_move(self, name=None): return bool(self.move) @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', methods=['_get_taxes', 'tax_date']) def _on_change_lines_taxes(self): pool = Pool() InvoiceTax = pool.get('account.invoice.tax') self.untaxed_amount = Decimal(0) self.tax_amount = Decimal(0) self.total_amount = Decimal(0) if self.lines: for line in self.lines: if getattr(line, 'type', '') == 'line': self.untaxed_amount += getattr(line, 'amount', 0) or 0 computed_taxes = self._get_taxes() else: computed_taxes = {} def is_zero(amount): if self.currency: return self.currency.is_zero(amount) else: return amount == Decimal(0) tax_keys = set() taxes = list(self.taxes or []) for tax in (self.taxes or []): if tax.manual: self.tax_amount += tax.amount or Decimal(0) continue key = tax._key if (key not in computed_taxes) or (key in tax_keys): taxes.remove(tax) continue tax_keys.add(key) if not is_zero( computed_taxes[key].base - (tax.base or Decimal(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) for key in computed_taxes: if key not in tax_keys: tax = computed_taxes[key].tax self.tax_amount += computed_taxes[key].amount value = InvoiceTax.default_get( list(InvoiceTax._fields.keys()), with_rec_name=False) value.update(computed_taxes[key].values()) value['manual'] = False value['description'] = tax.description value['legal_notice'] = tax.legal_notice value['currency'] = self.currency 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') cursor = Transaction().connection.cursor() untaxed_amount = {i.id: i.currency.round(Decimal(0)) for i in invoices} tax_amount = untaxed_amount.copy() total_amount = untaxed_amount.copy() invoices_no_cache = [] for invoice in invoices: if (invoice.total_amount_cache is not None and invoice.untaxed_amount_cache is not None and invoice.tax_amount_cache is not None): total_amount[invoice.id] = invoice.total_amount_cache untaxed_amount[invoice.id] = invoice.untaxed_amount_cache tax_amount[invoice.id] = invoice.tax_amount_cache else: invoices_no_cache.append(invoice.id) invoices_no_cache = cls.browse(invoices_no_cache) type_name = cls.tax_amount._field.sql_type().base tax = InvoiceTax.__table__() for sub_ids in grouped_slice(invoices_no_cache): red_sql = reduce_ids(tax.invoice, sub_ids) query = (tax.select(tax.invoice, Coalesce(Sum(tax.amount), 0).as_(type_name), where=red_sql, group_by=tax.invoice)) if backend.name == 'sqlite': sqlite_apply_types(query, [None, 'NUMERIC']) cursor.execute(*query) tax_amount.update(cursor) # Float amount must be rounded to get the right precision if backend.name == 'sqlite': for invoice in invoices: tax_amount[invoice.id] = invoice.currency.round( tax_amount[invoice.id]) for invoice in invoices_no_cache: zero = invoice.currency.round(Decimal(0)) untaxed_amount[invoice.id] = sum( (line.amount for line in invoice.line_lines), zero) 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 _query_lines_to_pay(cls, invoices): pool = Pool() MoveLine = pool.get('account.move.line') AdditionalMove = pool.get('account.invoice-additional-account.move') line = MoveLine.__table__() invoice = cls.__table__() additional_move = AdditionalMove.__table__() red_sql = reduce_ids(invoice.id, invoices) query = (invoice .join(line, condition=((invoice.move == line.move) & (invoice.account == line.account))) .select( invoice.id.as_('invoice'), line.id.as_('line'), line.maturity_date.as_('maturity_date'), line.reconciliation.as_('reconciliation'), where=red_sql)) query |= (invoice .join(additional_move, condition=additional_move.invoice == invoice.id) .join(line, condition=((additional_move.move == line.move) & (invoice.account == line.account))) .select( invoice.id.as_('invoice'), line.id.as_('line'), line.maturity_date.as_('maturity_date'), line.reconciliation.as_('reconciliation'), where=red_sql)) return query @classmethod def get_lines_to_pay(cls, invoices, name): cursor = Transaction().connection.cursor() lines = defaultdict(list) for sub_invoices in grouped_slice(invoices): query = cls._query_lines_to_pay(sub_invoices) query = query.select( query.invoice, query.line, order_by=query.maturity_date.nulls_last) cursor.execute(*query) for invoice_id, line_id in cursor: lines[invoice_id].append(line_id) return lines @classmethod def get_reconciliation_lines(cls, invoices, name): pool = Pool() Line = pool.get('account.move.line') Move = pool.get('account.move') line = Line.__table__() move = Move.__table__() cursor = Transaction().connection.cursor() lines = defaultdict(list) for sub_invoices in grouped_slice(invoices): sub_invoices = list(sub_invoices) lines_to_pay = cls._query_lines_to_pay(sub_invoices) query = cls._query_lines_to_pay(sub_invoices) query = ( query .join(line, condition=query.reconciliation == line.reconciliation) .join(move, condition=line.move == move.id) .select( query.invoice, line.id, where=~Exists( lines_to_pay.select( lines_to_pay.line, where=(lines_to_pay.invoice == query.invoice) & (lines_to_pay.line == line.id))), group_by=[query.invoice, line.id, move.date], order_by=move.date.asc)) cursor.execute(*query) for invoice_id, line_id in cursor: lines[invoice_id].append(line_id) return lines @classmethod def get_amount_to_pay(cls, invoices, name): pool = Pool() Currency = pool.get('currency.currency') Date = pool.get('ir.date') amounts = defaultdict(Decimal) for company, grouped_invoices in groupby( invoices, key=lambda i: i.company): with Transaction().set_context(company=company.id): today = Date.today() for invoice in grouped_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: 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 amounts[invoice.id] = amount_currency return amounts @classmethod def search_total_amount(cls, name, clause): pool = Pool() 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 Operator = fields.SQL_OPERATORS[operator] # SQLite uses float for sum if value is not None and 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(Round( (line.quantity * line.unit_price).cast(type_name), currency.digits), 0).as_('total_amount'), where=(invoice.total_amount_cache == Null)) | tax.select( tax.invoice.as_('invoice'), tax.amount.as_('total_amount'), where=Exists( invoice.select( invoice.id, where=(invoice.total_amount_cache == Null) & (invoice.id == tax.invoice))))) union |= invoice.select( invoice.id.as_('invoice'), invoice.total_amount_cache.as_('total_amount'), where=( (invoice.total_amount_cache != Null) & Operator(invoice.total_amount_cache.cast(type_name), value))) 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() 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 Operator = fields.SQL_OPERATORS[operator] # SQLite uses float for sum if value is not None and 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=(invoice.untaxed_amount_cache == Null), group_by=line.invoice, having=Operator( Coalesce(Sum(Round( (line.quantity * line.unit_price).cast( type_name), currency.digits)), 0).cast(type_name), value))) query |= invoice.select(invoice.id, where=( (invoice.untaxed_amount_cache != Null) & Operator( invoice.untaxed_amount_cache.cast(type_name), value))) return [('id', 'in', query)] @classmethod def search_tax_amount(cls, name, clause): pool = Pool() Tax = pool.get('account.invoice.tax') Invoice = pool.get('account.invoice') type_name = cls.tax_amount._field.sql_type().base tax = Tax.__table__() invoice = Invoice.__table__() _, operator, value = clause Operator = fields.SQL_OPERATORS[operator] # SQLite uses float for sum if value is not None and backend.name == 'sqlite': value = float(value) query = tax.select(tax.invoice, where=Exists( invoice.select( invoice.id, where=(invoice.tax_amount_cache == Null) & (invoice.id == tax.invoice))), group_by=tax.invoice, having=Operator( Coalesce(Sum(tax.amount), 0).cast(type_name), value)) query |= invoice.select(invoice.id, where=( (invoice.tax_amount_cache != Null) & Operator(invoice.tax_amount_cache.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 @classmethod def get_has_payment_method(cls, invoices, name): pool = Pool() Method = pool.get('account.invoice.payment.method') methods = {} for (company, account), sub_invoices in groupby( invoices, key=lambda i: (i.company, i.account)): sub_invoice_ids = [i.id for i in sub_invoices] value = bool(Method.search([ ('company', '=', company.id), ('debit_account', '!=', account.id), ('credit_account', '!=', account.id), ], limit=1)) methods.update(dict.fromkeys(sub_invoice_ids, value)) return methods @classmethod def get_has_report_cache(cls, invoices, name): table = cls.__table__() cursor = Transaction().connection.cursor() result = {} has_cache = ( (table.invoice_report_cache_id != Null) | (table.invoice_report_cache != Null)) for sub_invoices in grouped_slice(invoices): sub_ids = map(int, sub_invoices) cursor.execute(*table.select(table.id, has_cache, where=reduce_ids(table.id, sub_ids))) result.update(cursor) return result @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 @fields.depends('accounting_date', 'invoice_date', 'company') def tax_date(self): pool = Pool() Date = pool.get('ir.date') context = Transaction().context with Transaction().set_context( company=self.company.id if self.company else context.get('company')): today = Date.today() return self.accounting_date or self.invoice_date or today @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): for key, tax_line in self._get_taxes().items(): value = dict(tax_line.values()) value['invoice'] = self.id value['manual'] = False value['description'] = tax_line.tax.description value['legal_notice'] = tax_line.tax.legal_notice yield key, value @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 = dict(invoice._compute_taxes()) if not invoice.taxes: to_create.extend(computed_taxes.values()) else: tax_keys = set() 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.add(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: line.amount_second_currency = amount line.second_currency = self.currency with Transaction().set_context(date=self.currency_date): amount = Currency.compute( self.currency, amount, self.company.currency) else: line.amount_second_currency = None line.second_currency = None if amount >= 0: if self.type == 'out': line.debit, line.credit = amount, 0 else: line.debit, line.credit = 0, amount else: if self.type == 'out': line.debit, line.credit = 0, -amount else: line.debit, line.credit = -amount, 0 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: if self.alternative_payees: line.party, = self.alternative_payees else: line.party = self.party line.maturity_date = date line.description = self.description return line def _get_exchange_move_line(self, amount): pool = Pool() Configuration = pool.get('account.configuration') MoveLine = pool.get('account.move.line') configuration = Configuration(1) line = MoveLine() line.debit = -amount if amount < 0 else 0 line.credit = amount if amount > 0 else 0 if line.credit: line.account = configuration.get_multivalue( 'currency_exchange_credit_account', company=self.company.id) if not line.account: raise AccountMissing(gettext( 'account_invoice.' 'msg_invoice_currency_exchange_credit_account_missing', invoice=self.rec_name, company=self.company.rec_name)) else: line.account = configuration.get_multivalue( 'currency_exchange_debit_account', company=self.company.id) if not line.account: raise AccountMissing(gettext( 'account_invoice.' 'msg_invoice_currency_exchange_debit_account_missing', invoice=self.rec_name, company=self.company.rec_name)) line.amount_second_currency = None line.second_currency = None 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') if self.move: return self.move with Transaction().set_context(company=self.company.id): today = Date.today() self.update_taxes(exception=True) move_lines = [] for line in self.line_lines: move_lines += line.get_move_lines() for tax in self.taxes: move_lines += tax.get_move_lines() remainder = sum(l.debit - l.credit for l in move_lines) if self.payment_term: payment_date = self.payment_term_date or self.invoice_date or today term_lines = self.payment_term.compute( self.total_amount, self.currency, payment_date) else: term_lines = [(self.payment_term_date or today, self.total_amount)] past_payment_term_dates = [] for date, amount in term_lines: line = self._get_move_line(date, amount) move_lines.append(line) remainder += line.debit - line.credit if self.type == 'out' and date < today: past_payment_term_dates.append(date) if self.currency != self.company.currency and remainder: line = self._get_exchange_move_line(remainder) move_lines.append(line) if any(past_payment_term_dates): lang = Lang.get() warning_key = Warning.format('invoice_payment_term', [self]) 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(min(past_payment_term_dates)))) accounting_date = self.accounting_date or self.invoice_date or today period = Period.find(self.company, date=accounting_date) move = Move() move.journal = self.journal move.period = period 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') sequences = set() for company, grouped_invoices in groupby( invoices, key=lambda i: i.company): with Transaction().set_context(company=company.id): today = Date.today() def invoice_date(invoice): return invoice.invoice_date or today to_number = defaultdict(list) grouped_invoices = sorted(grouped_invoices, key=invoice_date) for invoice in grouped_invoices: # Posted, paid and cancelled invoices are tested by # check_modify so we can not modify tax_identifier nor number if invoice.state in {'posted', 'paid', 'cancelled'}: 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.sequence_type_cache = invoice._sequence_type sequence, sequence_date = invoice._number_sequence() to_number[(sequence, sequence_date)].append(invoice) if invoice.type == 'out' and sequence not in sequences: date = invoice_date(invoice) # Do not need to lock the table # because sequence.get_many is sequential after_invoices = cls.search([ ('sequence', '=', sequence), ('invoice_date', '>', date), ], limit=1, order=[('invoice_date', 'DESC')]) if after_invoices: after_invoice, = after_invoices raise InvoiceNumberError( gettext('account_invoice.msg_invoice_number_after', invoice=invoice.rec_name, sequence=sequence.rec_name, date=Lang.get().strftime(date), after_invoice=after_invoice.rec_name)) sequences.add(sequence) for (sequence, date), n_invoices in to_number.items(): with Transaction().set_context( date=date, company=company.id): for invoice, number in zip( n_invoices, sequence.get_many(len(n_invoices))): invoice.sequence = sequence invoice.number = number cls.save(invoices) def _number_sequence(self, pattern=None): "Returns the sequence and date to use for numbering" 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 = Period.find( self.company, date=accounting_date, test_state=self.type != 'in') 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): return getattr( invoice_sequence, self._sequence_field), accounting_date else: raise InvoiceNumberError( gettext('account_invoice.msg_invoice_no_sequence', invoice=self.rec_name, fiscalyear=fiscalyear.rec_name)) @property def _sequence_type(self): if (all(l.amount <= 0 for l in self.line_lines) and self.total_amount < 0): return 'credit_note' else: return 'invoice' @property def sequence_type(self): return self.sequence_type_cache or self._sequence_type @property def _sequence_field(self): "Returns the field name of invoice_sequence to use" return f'{self.type}_{self.sequence_type}_sequence' def get_tax_identifier(self, pattern=None): "Return the default computed tax identifier" pattern = pattern.copy() if pattern is not None else {} if self.invoice_address and self.invoice_address.country: pattern.setdefault('country', self.invoice_address.country.id) return self.company.get_tax_identifier(pattern) @property def invoice_report_versioned(self): return self.state in {'posted', 'paid'} and self.type == 'out' def create_invoice_report_revision(self): pool = Pool() InvoiceReportRevision = pool.get('account.invoice.report.revision') if not self.invoice_report_versioned: return invoice_report_revision = InvoiceReportRevision( invoice=self, invoice_report_cache=self.invoice_report_cache, invoice_report_cache_id=self.invoice_report_cache_id, invoice_report_format=self.invoice_report_format) self.invoice_report_revisions += (invoice_report_revision,) self.invoice_report_cache = None self.invoice_report_cache_id = None self.invoice_report_format = None return invoice_report_revision @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))) 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): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [bool_op, ('number', *clause[1:]), ('reference', *clause[1:]), ] def get_origins(self, name): return ', '.join(set(filter(None, (l.origin_name for l in self.line_lines)))) def chat_language(self, audience='internal'): language = super().chat_language(audience=audience) if audience == 'public': language = self.party.lang.code if self.party.lang else None return language @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 check_modification(cls, mode, invoices, values=None, external=False): super().check_modification( mode, invoices, values=values, external=external) if (mode == 'delete' or (mode == 'write' and values.keys() - cls._check_modify_exclude)): for invoice in invoices: if not invoice.is_modifiable: raise AccessError(gettext( 'account_invoice.msg_invoice_modify', invoice=invoice.rec_name)) if mode == 'delete': for invoice in invoices: if invoice.state not in {'cancelled', 'draft'}: 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)) @classmethod def on_modification(cls, mode, invoices, field_names=None): super().on_modification(mode, invoices, field_names=field_names) if mode != 'delete': if draft_invoices := [i for i in invoices if i.state == 'draft']: cls.update_taxes(draft_invoices) def compute_fields(self, field_names=None): values = super().compute_fields(field_names=field_names) if (field_names is None or 'number' in field_names): values['number_alnum'] = ( re.sub(r'[\W_]', '', self.number) if self.number is not None else None) try: values['number_digit'] = int( re.sub(r'\D', '', self.number or '')[-18:]) except ValueError: values['number_digit'] = None return values @classmethod def copy(cls, invoices, default=None): if default is None: default = {} else: default = default.copy() alternative_payees2copy = set() for invoice in invoices: if len(invoice.alternative_payees) == 1: parties = {l.party for l in invoice.lines_to_pay} if parties <= set(invoice.alternative_payees): alternative_payees2copy.add(invoice.id) def copy_alternative_payees(data): if data['id'] in alternative_payees2copy: return data.get('alternative_payees', []) else: return [] default.setdefault('number', None) default.setdefault('number_alnum', None) default.setdefault('number_digit', None) default.setdefault('reference') default.setdefault('supplier_payment_reference') default.setdefault('supplier_payment_reference_type') default.setdefault('sequence') default.setdefault('move', None) default.setdefault('additional_moves', 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('alternative_payees', copy_alternative_payees) default.setdefault('payment_lines', None) default.setdefault('invoice_date', None) default.setdefault('accounting_date', None) default.setdefault('payment_term_date', None) default.setdefault('total_amount_cache', None) default.setdefault('untaxed_amount_cache', None) default.setdefault('tax_amount_cache', None) default.setdefault('validated_by') default.setdefault('posted_by') default.setdefault('invoice_report_revisions', None) return super().copy(invoices, default=default) @classmethod def validate(cls, invoices): super().validate(invoices) for invoice in invoices: invoice.check_payment_lines() def check_payment_lines(self): def balance(line): if self.currency == line.second_currency: return line.amount_second_currency elif self.currency == self.company.currency: return line.debit - line.credit else: return 0 amount = sum(map(balance, self.lines_to_pay)) payment_amount = sum(map(balance, self.payment_lines)) if abs(amount) < abs(payment_amount): raise InvoiceValidationError( gettext('account_invoice' '.msg_invoice_payment_lines_greater_amount', invoice=self.rec_name)) @classmethod def validate_fields(cls, invoices, field_names): super().validate_fields(invoices, field_names) cls.check_supplier_payment_reference(invoices, field_names) @classmethod def check_supplier_payment_reference(cls, invoices, field_names): if field_names and not ( field_names & { 'supplier_payment_reference', 'supplier_payment_reference_type', }): return for invoice in invoices: if type := invoice.supplier_payment_reference_type: method = getattr( cls, f'_check_supplier_payment_reference_{type}') if not method(invoice): reference = invoice.supplier_payment_reference type = invoice.supplier_payment_reference_type_string raise InvoiceValidationError( gettext( 'account_invoice.' 'msg_invoice_supplier_payment_reference_invalid', type=type, reference=reference, invoice=invoice.rec_name)) def _check_supplier_payment_reference_creditor_reference(self): return iso11649.is_valid(self.supplier_payment_reference) def get_reconcile_lines_for_amount(self, amount, currency, party=None): ''' Return list of lines and the remainder to make reconciliation. ''' pool = Pool() Line = pool.get('account.move.line') assert currency in {self.currency, self.company.currency} if party is None: party = self.party lines = [ l for l in self.payment_lines + self.lines_to_pay if not l.reconciliation and (not self.account.party_required or l.party == party)] return Line.find_best_reconciliation( lines, currency, amount=amount) def pay_invoice( self, amount, payment_method, date, description=None, overpayment=0, party=None): ''' 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() Currency = pool.get('currency.currency') Move = pool.get('account.move') Line = pool.get('account.move.line') Period = pool.get('account.period') if party is None: party = self.party pay_line = Line(account=self.account) counterpart_line = Line() lines = [pay_line, counterpart_line] pay_amount = amount - overpayment if self.currency != self.company.currency: amount_second_currency = pay_amount second_currency = self.currency overpayment_second_currency = overpayment with Transaction().set_context(date=date): amount = Currency.compute( self.currency, amount, self.company.currency) overpayment = Currency.compute( self.currency, overpayment, self.company.currency) pay_amount = amount - overpayment else: amount_second_currency = None second_currency = None overpayment_second_currency = None 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 amount_second_currency is not None: pay_line.amount_second_currency = ( amount_second_currency.copy_sign( pay_line.debit - pay_line.credit)) pay_line.second_currency = second_currency 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) if overpayment_second_currency is not None: overpayment_line.amount_second_currency = ( overpayment_second_currency.copy_sign( overpayment_line.debit - overpayment_line.credit)) overpayment_line.second_currency = second_currency 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) if amount_second_currency is not None: counterpart_line.amount_second_currency = ( amount_second_currency.copy_sign( counterpart_line.debit - counterpart_line.credit)) counterpart_line.second_currency = second_currency for line in lines: if line.account.party_required: line.party = party period = Period.find(self.company, date=date) move = Move( journal=payment_method.journal, period=period, 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) @dualmethod def print_invoice(cls, invoices): ''' Generate invoice report and store it in invoice_report field. ''' InvoiceReport = Pool().get('account.invoice', type='report') for invoice in invoices: if not invoice.invoice_report_cache: InvoiceReport.execute([invoice.id], {}) def _credit(self, **values): ''' Return values to credit invoice. ''' credit = self.__class__(**values) for field in [ 'company', 'tax_identifier', 'party', 'party_tax_identifier', '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) 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 def _store_cache(cls, invoices): invoices = list(invoices) cls.write(invoices, { 'untaxed_amount_cache': None, 'tax_amount_cache': None, 'total_amount_cache': None, }) for invoice in invoices: invoice.untaxed_amount_cache = invoice.untaxed_amount invoice.tax_amount_cache = invoice.tax_amount invoice.total_amount_cache = invoice.total_amount cls.save(invoices) @classmethod @ModelView.button @Workflow.transition('draft') @reset_employee('validated_by', 'posted_by') def draft(cls, invoices): Move = Pool().get('account.move') cls.write(invoices, { 'tax_amount_cache': None, 'untaxed_amount_cache': None, 'total_amount_cache': None, }) moves = [] for invoice in invoices: if invoice.move: moves.append(invoice.move) if invoice.additional_moves: moves.extend(invoice.additional_moves) if len(invoice.alternative_payees) > 1: invoice.alternative_payees = [] cls.save(invoices) if moves: Move.delete(moves) @classmethod @ModelView.button @Workflow.transition('validated') @set_employee('validated_by') def validate_invoice(cls, invoices): pool = Pool() Move = pool.get('account.move') cls._check_taxes(invoices) cls._check_similar(invoices) invoices_in = cls.browse([i for i in invoices if i.type == 'in']) cls.set_number(invoices_in) cls._store_cache(invoices) 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') transaction = Transaction() context = transaction.context cls.set_number(invoices) for company, grouped_invoices in groupby( invoices, key=lambda i: i.company): with Transaction().set_context(company=company.id): today = Date.today() for invoice in grouped_invoices: if not invoice.payment_term_date: invoice.payment_term_date = today cls.save(invoices) with transaction.set_context( _skip_warnings=True, queue_batch=context.get('queue_batch', True)): cls.__queue__._post(invoices) @classmethod @ModelView.button @Workflow.transition('posted') @set_employee('posted_by', when='before') def post(cls, invoices): pool = Pool() Date = pool.get('ir.date') Warning = pool.get('res.user.warning') for company, grouped_invoices in groupby( invoices, key=lambda i: i.company): with Transaction().set_context(company=company.id): today = Date.today() future_invoices = [ i for i in grouped_invoices if i.type == 'out' and i.invoice_date and i.invoice_date > today] if future_invoices: names = ', '.join(m.rec_name for m in future_invoices[:5]) if len(future_invoices) > 5: names += '...' warning_key = Warning.format( 'invoice_date_future', future_invoices) if Warning.check(warning_key): raise InvoiceFutureWarning(warning_key, gettext('account_invoice.msg_invoice_date_future', invoices=names)) to_check = [i for i in invoices if i.state != 'validated'] cls._check_taxes(to_check) cls._check_similar(to_check) cls._post(invoices) @classmethod def _post(cls, invoices): pool = Pool() Move = pool.get('account.move') transaction = Transaction() context = transaction.context cls.set_number(invoices) cls._store_cache(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 = [] to_print = [] for invoice in invoices: if invoice.type == 'out': to_print.append(invoice) if invoice.reconciled: reconciled.append(invoice) if to_print: cls.__queue__.print_invoice(to_print) if reconciled: with transaction.set_context( queue_batch=context.get('queue_batch', True)): cls.__queue__.process(reconciled) @classmethod def _check_taxes(cls, invoices): pool = Pool() Line = pool.get('account.invoice.line') Warning = pool.get('res.user.warning') for invoice in invoices: different_lines = [] for line in invoice.line_lines: test_line = Line(line.id) test_line.on_change_product() if (set(test_line.taxes) != set(line.taxes) or test_line.taxes_deductible_rate != line.taxes_deductible_rate): different_lines.append(line) if different_lines: warning_key = Warning.format( 'invoice_taxes', [invoice]) if Warning.check(warning_key): lines = ', '.join(l.rec_name for l in different_lines[:5]) if len(different_lines) > 5: lines += '...' raise InvoiceTaxesWarning(warning_key, gettext('account_invoice.msg_invoice_default_taxes', invoice=invoice.rec_name, lines=lines)) @classmethod def _check_similar(cls, invoices, type='in'): pool = Pool() Warning = pool.get('res.user.warning') for sub_invoices in grouped_slice(invoices): sub_invoices = list(sub_invoices) domain = list(filter(None, (i._similar_domain() for i in sub_invoices if i.type == type))) if not domain: continue if cls.search(['OR'] + domain, order=[]): for invoice in sub_invoices: domain = invoice._similar_domain() if not domain: continue try: similar, = cls.search(domain, limit=1) except ValueError: continue warning_key = Warning.format( 'invoice_similar', [invoice]) if Warning.check(warning_key): raise InvoiceSimilarWarning(warning_key, gettext('account_invoice.msg_invoice_similar', similar=similar.rec_name, invoice=invoice.rec_name)) def _similar_domain(self, delay=None): pool = Pool() Date = pool.get('ir.date') if not self.reference: return with Transaction().set_context(company=self.company.id): invoice_date = self.invoice_date or Date.today() if delay is None: delay = dt.timedelta(days=60) return [ ('company', '=', self.company.id), ('type', '=', self.type), ('party', '=', self.party.id), ('reference', '=', self.reference), ('id', '!=', self.id), ['OR', ('invoice_date', '=', None), [ ('invoice_date', '>=', invoice_date - delay), ('invoice_date', '<=', invoice_date + delay), ], ], ] @classmethod @ModelView.button_action('account_invoice.wizard_pay') def pay(cls, invoices): pass @classmethod @ModelView.button_action( 'account_invoice.act_reschedule_lines_to_pay_wizard') def reschedule_lines_to_pay(cls, invoices): pass @classmethod @ModelView.button_action( 'account_invoice.act_delegate_lines_to_pay_wizard') def delegate_lines_to_pay(cls, invoices): pass @classmethod @ModelView.button def process(cls, invoices): to_save = [] paid = [] posted = [] for invoice in invoices: if invoice.state in {'posted', 'paid'}: if invoice.reconciled: paid.append(invoice) else: posted.append(invoice) elif invoice.state == 'cancelled' and invoice.move: if not invoice.reconciled: if invoice.cancel_move: invoice.cancel_move = None invoice.save() to_save.append(invoice) posted.append(invoice) cls.save(to_save) 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 and invoice.move.state == 'draft': delete_moves.append(invoice.move) delete_moves.extend(invoice.additional_moves) 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)) if invoice.move: invoice.cancel_move = invoice.move.cancel() additional_cancel_moves = [ m.cancel() for m in invoice.additional_moves] invoice.additional_moves += tuple( additional_cancel_moves) to_save.append(invoice) cancel_moves.append(invoice.cancel_move) cancel_moves.extend(additional_cancel_moves) if cancel_moves: Move.save(cancel_moves) cls._store_cache(invoices) 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 move in chain( [invoice.move, invoice.cancel_move], invoice.additional_moves): for line in move.lines: if (not line.reconciliation and 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 InvoiceAdditionalMove(ModelSQL): __name__ = 'account.invoice-additional-account.move' invoice = fields.Many2One( 'account.invoice', "Invoice", ondelete='CASCADE', required=True) move = fields.Many2One( 'account.move', "Additional Move", ondelete='CASCADE') class AlternativePayee(ModelSQL): __name__ = 'account.invoice.alternative_payee' invoice = fields.Many2One( 'account.invoice', "Invoice", ondelete='CASCADE', required=True) party = fields.Many2One( 'party.party', "Payee", ondelete='RESTRICT', required=True) class InvoicePaymentLine(ModelSQL): __name__ = 'account.invoice-account.move.line' invoice = fields.Many2One( 'account.invoice', "Invoice", ondelete='CASCADE', 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') invoice_alternative_payees = fields.Function( fields.Many2Many( 'party.party', None, None, "Invoice Alternative Payees"), 'get_invoice') line = fields.Many2One( 'account.move.line', "Payment Line", ondelete='CASCADE', required=True, domain=[ ('account', '=', Eval('invoice_account', -1)), ['OR', ('party', '=', Eval('invoice_party', -1)), ('party', 'in', Eval('invoice_alternative_payees', [])), ], ]) @classmethod def __setup__(cls): super().__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 invoice_alternative_payees = 'invoice_alternative_payees' 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 if invoice_alternative_payees: result['invoice_alternative_payees'][record.id] = [ p.id for p in record.invoice.alternative_payees] return result class InvoiceLine(sequence_ordered(), ModelSQL, ModelView, TaxableMixin): __name__ = 'account.invoice.line' _states = { 'readonly': Eval('invoice_state') != 'draft', } invoice = fields.Many2One( 'account.invoice', "Invoice", ondelete='CASCADE', states={ 'required': (~Eval('invoice_type') & Eval('party') & Eval('currency') & Eval('company')), 'invisible': Bool(Eval('context', {}).get('standalone')), 'readonly': _states['readonly'] & Bool(Eval('invoice')), }) invoice_party = fields.Function( fields.Many2One( 'party.party', "Party", context={ 'company': Eval('company', -1), 'party_contact_mechanism_usage': 'invoice', }, depends=['company']), 'on_change_with_invoice_party', searcher='search_invoice_party') invoice_description = fields.Function( fields.Char("Invoice Description"), 'on_change_with_invoice_description', searcher='search_invoice_description') 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", states={ 'readonly': Eval('context', {}).get('type') | Eval('type'), 'required': ~Eval('invoice'), }) party = fields.Many2One( 'party.party', "Party", states={ 'required': ~Eval('invoice'), 'readonly': _states['readonly'], }, context={ 'company': Eval('company', -1), 'party_contact_mechanism_usage': 'invoice', }, depends={'company'}) party_lang = fields.Function(fields.Char('Party Language'), 'on_change_with_party_lang') currency = fields.Many2One( 'currency.currency', "Currency", required=True, states=_states) company = fields.Many2One( 'company.company', "Company", required=True, states=_states, context={ 'party_contact_mechanism_usage': 'invoice', }) type = fields.Selection([ ('line', 'Line'), ('subtotal', 'Subtotal'), ('title', 'Title'), ('comment', 'Comment'), ], "Type", required=True, states={ 'invisible': Bool(Eval('context', {}).get('standalone')), 'readonly': _states['readonly'], }) quantity = fields.Float( "Quantity", digits='unit', states={ 'invisible': Eval('type') != 'line', 'required': Eval('type') == 'line', 'readonly': _states['readonly'], }) 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)), ]) 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={'company'}) product_uom_category = fields.Function( fields.Many2One( 'product.uom.category', "Product UoM Category", help="The category of Unit of Measure for the product."), '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={'invoice'}) unit_price = Monetary( "Unit Price", currency='currency', digits=price_digits, states={ 'invisible': Eval('type') != 'line', 'required': Eval('type') == 'line', 'readonly': _states['readonly'], }) amount = fields.Function(Monetary( "Amount", currency='currency', digits='currency', states={ 'invisible': ~Eval('type').in_(['line', 'subtotal']), }), 'get_amount') description = fields.Text( "Description", states={ 'readonly': (_states['readonly'] & ~Id('account', 'group_account_admin').in_( Eval('context', {}).get('groups', []))), }) summary = fields.Function( fields.Char('Summary'), 'on_change_with_summary', searcher='search_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={'invoice'}) taxes_deductible_rate = fields.Numeric( "Taxes Deductible Rate", digits=(None, 10), domain=[ ('taxes_deductible_rate', '>=', 0), ('taxes_deductible_rate', '<=', 1), ], states={ 'invisible': ( (Eval('invoice_type') != 'in') | (Eval('type') != 'line')), }) taxes_date = fields.Date( "Taxes Date", states={ 'invisible': Eval('type') != 'line', 'readonly': _states['readonly'], }, 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', states=_states) del _states @classmethod def __setup__(cls): super().__setup__() cls._check_modify_exclude = {'note', 'origin', 'description'} # Set account domain dynamically for kind cls.account.domain = [ ('closed', '!=', True), ('company', '=', Eval('company', -1)), ('id', '!=', Eval('_parent_invoice', {}).get('account', -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.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 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_company(): return Transaction().context.get('company') @staticmethod def default_type(): return 'line' @fields.depends('party', 'invoice', '_parent_invoice.party') def on_change_with_invoice_party(self, name=None): if self.invoice and self.invoice.party: return self.invoice.party elif self.party: return self.party @classmethod def search_invoice_party(cls, name, clause): nested = clause[0][len(name):] return ['OR', ('invoice.party' + nested, *clause[1:]), ('party' + nested, *clause[1:]), ] @fields.depends('invoice', '_parent_invoice.description') def on_change_with_invoice_description(self, name=None): if self.invoice: return self.invoice.description @classmethod def search_invoice_description(cls, name, clause): return [('invoice.description', *clause[1:])] @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: state = self.invoice.state if state == 'cancelled' and self.invoice.cancel_move: state = 'paid' else: state = 'draft' return state @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 '') @classmethod def search_summary(cls, name, clause): return [('description', *clause[1:])] @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)) * (self.unit_price or Decimal(0))) invoice_type = ( self.invoice.type if self.invoice else self.invoice_type) if (invoice_type == 'in' and self.taxes_deductible_rate is not None 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) 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__) and self.origin.id >= 0: return self.origin.invoice.rec_name if self.origin and self.origin.id >= 0: return self.origin.rec_name @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 deductible_rate is None: deductible_rate = 1 if not deductible_rate: return [] else: deductible_rate = 1 return [( list(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 else: return super().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 = 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', 'taxes', '_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 = set() 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.update(tax_ids) continue taxes.add(tax.id) if party and party.supplier_tax_rule: tax_ids = party.supplier_tax_rule.apply(None, pattern) if tax_ids: taxes.update(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 = set() 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.update(tax_ids) continue taxes.add(tax.id) if party and party.customer_tax_rule: tax_ids = party.customer_tax_rule.apply(None, pattern) if tax_ids: taxes.update(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 @cached_property def product_name(self): return self.product.rec_name if self.product else '' @fields.depends('product') def on_change_with_product_uom_category(self, name=None): return self.product.default_uom_category if self.product else None @fields.depends( 'account', 'product', 'invoice', 'taxes', '_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 = set() 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.update(tax_ids) continue taxes.add(tax.id) if tax_rule: tax_ids = tax_rule.apply(None, pattern) if tax_ids: taxes.update(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_number_symbol( self.quantity or 0, self.unit, digits=self.unit.digits) + ' %s' % 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): _, operator, value = clause if operator.startswith('!') or operator.startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [bool_op, ('invoice.rec_name', *clause[1:]), ('product.rec_name', *clause[1:]), ('account.rec_name', *clause[1:]), ] @classmethod def check_modification(cls, mode, lines, values=None, external=False): super().check_modification( mode, lines, values=values, external=external) if mode == 'create': for line in lines: if line.invoice and line.invoice.state != 'draft': raise AccessError(gettext( 'account_invoice.msg_invoice_line_create_draft', invoice=line.invoice.rec_name)) elif (mode == 'delete' or (mode == 'write' and values.keys() - 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 copy(cls, lines, default=None): if default is None: default = {} else: default = default.copy() default.setdefault('origin', None) return super().copy(lines, default=default) 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', 'party', 'currency', 'company', 'unit_price', 'description', 'unit', 'product', 'account', 'taxes_deductible_rate']: setattr(line, field, getattr(self, field)) line.taxes_date = self.tax_date line.taxes = self.taxes return line class InvoiceLineTax(ModelSQL): __name__ = 'account.invoice.line-account.tax' line = fields.Many2One( 'account.invoice.line', "Invoice Line", ondelete='CASCADE', required=True) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', required=True) @classmethod def __setup__(cls): super().__setup__() t = cls.__table__() cls._sql_constraints += [ ('line_tax_unique', Unique(t, t.line, t.tax), 'account_invoice.msg_invoice_line_tax_unique'), ] @classmethod def __register__(cls, module): # Migration from 7.0: rename to standard name backend.TableHandler.table_rename( 'account_invoice_line_account_tax', cls._table) super().__register__(module) class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): __name__ = 'account.invoice.tax' _rec_name = 'description' _states = { 'readonly': Eval('invoice_state') != 'draft', } invoice = fields.Many2One( 'account.invoice', "Invoice", ondelete='CASCADE', required=True, states={ 'readonly': _states['readonly'] & Bool(Eval('invoice')), }) 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={ 'readonly': (_states['readonly'] & ~Id('account', 'group_account_admin').in_( Eval('context', {}).get('groups', []))), }) 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)), ('id', '!=', Eval('_parent_invoice', {}).get('account', -1)), ], states=_states, depends={'invoice'}) base = Monetary( "Base", currency='currency', digits='currency', required=True, states=_states) amount = Monetary( "Amount", currency='currency', digits='currency', required=True, states=_states, depends={'tax', 'base', 'manual'}) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') manual = fields.Boolean('Manual', states=_states) tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT', domain=[ ['OR', ('group', '=', None), ('group.kind', 'in', If(Eval('_parent_invoice', {}).get('type') == 'out', ['sale', 'both'], ['purchase', 'both']), )], ('company', '=', Eval('_parent_invoice', {}).get('company', 0)), ], states={ 'readonly': ( ~Eval('manual', False) | ~Bool(Eval('invoice')) | _states['readonly']), }, depends={'invoice'}) legal_notice = fields.Text( "Legal Notice", states={ 'readonly': (_states['readonly'] & ~Id('account', 'group_account_admin').in_( Eval('context', {}).get('groups', []))), }) del _states @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('invoice') cls._check_modify_exclude = {'description', 'legal_notice'} @staticmethod def default_base(): return Decimal(0) @staticmethod def default_amount(): return Decimal(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): return self.invoice.currency if self.invoice else None @fields.depends( 'tax', 'invoice', '_parent_invoice.party', 'base', methods=['_compute_amount']) 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 is not None: if self.base >= 0: self.account = tax.invoice_account else: self.account = tax.credit_note_account self._compute_amount() @fields.depends('base', 'tax', methods=['_compute_amount']) def on_change_base(self): if self.base is not None and self.tax: if self.base >= 0: self.account = self.tax.invoice_account else: self.account = self.tax.credit_note_account self._compute_amount() @fields.depends( 'tax', 'base', 'manual', 'invoice', '_parent_invoice.currency', # From_date '_parent_invoice.accounting_date', '_parent_invoice.invoice_date', '_parent_invoice.company') def _compute_amount(self): pool = Pool() Tax = pool.get('account.tax') if self.tax and self.manual: tax = self.tax base = self.base or Decimal(0) if self.invoice and self.invoice.tax_date: tax_date = self.invoice.tax_date for values in Tax.compute([tax], base, 1, tax_date): if (values['tax'] == tax and values['base'] == base): amount = values['amount'] if self.invoice.currency: amount = self.invoice.currency.round(amount) self.amount = amount @property def _key(self): # Same as _TaxLine return (self.account, self.tax, (getattr(self, 'base', 0) or 0) >= 0) @classmethod def check_modification(cls, mode, taxes, values=None, external=False): super().check_modification( mode, taxes, values=values, external=external) if mode == 'create': for tax in taxes: if tax.invoice.state != 'draft': raise AccessError(gettext( 'account_invoice.msg_invoice_line_create_draft', invoice=tax.invoice.rec_name)) elif (mode == 'delete' or (mode == 'write' and values.keys() - cls._check_modify_exclude)): 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 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) base = Currency.compute(self.invoice.currency, self.base, self.invoice.company.currency) line.amount_second_currency = self.amount line.second_currency = self.invoice.currency else: amount = self.amount base = self.base 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_lines = [] tax_line = TaxLine() tax_line.amount = amount tax_line.type = 'tax' tax_line.tax = self.tax tax_lines.append(tax_line) if self.manual: tax_line = TaxLine() tax_line.amount = base tax_line.type = 'base' tax_line.tax = self.tax tax_lines.append(tax_line) line.tax_lines = tax_lines 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): __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', -1)), ]) debit_account = fields.Many2One('account.account', "Debit Account", required=True, domain=[ ('type', '!=', None), ('closed', '!=', True), ('company', '=', Eval('company', -1)), ]) @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def default_company(cls): return Transaction().context.get('company') class InvoiceReportRevision(ModelSQL, ModelView, InvoiceReportMixin): __name__ = 'account.invoice.report.revision' invoice = fields.Many2One( 'account.invoice', "Invoice", required=True, ondelete='CASCADE') date = fields.DateTime("Date", required=True, readonly=True) filename = fields.Function(fields.Char("File Name"), 'get_filename') @classmethod def __setup__(cls): super().__setup__() cls.__access__.add('invoice') cls._order.insert(0, ('date', 'DESC')) cls.invoice_report_cache.filename = 'filename' @classmethod def default_date(cls): return dt.datetime.now() @classmethod def get_filename(cls, revisions, name): pool = Pool() ActionReport = pool.get('ir.action.report') action_report, = ActionReport.search([ ('report_name', '=', 'account.invoice'), ], limit=1) action_report_name = action_report.name[:100] if action_report.record_name: template = TextTemplate(action_report.record_name) else: template = None filenames = {} for revision in revisions: invoice = revision.invoice if template: record_name = template.generate(record=invoice).render() else: record_name = invoice.rec_name filename = '-'.join([action_report_name, record_name]) filenames[revision.id] = ( f'{slugify(filename)}.{revision.invoice_report_format}') return filenames class RefreshInvoiceReport(Wizard): __name__ = 'account.invoice.refresh_invoice_report' start_state = 'archive' archive = StateTransition() print_ = StateReport('account.invoice') def transition_archive(self): for record in self.records: record.create_invoice_report_revision() self.model.save(self.records) return 'print_' def do_print_(self, action): ids = [r.id for r in self.records] return action, {'ids': ids} class InvoiceReport(Report): __name__ = 'account.invoice' @classmethod def __setup__(cls): super().__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, invoice.invoice_report_cache) else: result = super()._execute(records, header, data, action) if invoice.invoice_report_versioned: format_, data = result if isinstance(data, str): data = bytes(data, 'utf-8') 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'] with Transaction().set_context(company=context['invoice'].company.id): context['today'] = Date.today() return context class InvoiceEdocument(Wizard): __name__ = 'account.invoice.edocument' start = StateView( 'account.invoice.edocument.start', 'account_invoice.edocument_start_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Render", 'render', 'tryton-ok', default=True), ]) render = StateTransition() result = StateView( 'account.invoice.edocument.result', 'account_invoice.edocument_result_view_form', [ Button("Close", 'end', 'tryton-close', default=True), ]) def transition_render(self): pool = Pool() Start = pool.get('account.invoice.edocument.start') if self.start.format not in dict(Start.format.selection): raise ValueError("Unsupported format") Edocument = pool.get(self.start.format) edocument = Edocument(self.record) file = edocument.render(self.start.template) if isinstance(file, str): file = file.decode('utf-8') self.result.file = file self.result.filename = edocument.filename return 'result' def default_result(self, fields): file = self.result.file self.result.file = None # No need to store it in the session return { 'file': file, 'filename': self.result.filename, } class InvoiceEdocumentStart(ModelView): __name__ = 'account.invoice.edocument.start' format = fields.Selection([ ], "Format", required=True) template = fields.Selection('get_templates', "Template", required=True) @fields.depends() def get_templates(self): return [] class InvoiceEdocumentResult(ModelView): __name__ = 'account.invoice.edocument.result' file = fields.Binary("File", readonly=True, filename='filename') filename = fields.Char("File Name", readonly=True) class PayInvoiceStart(ModelView): __name__ = 'account.invoice.pay.start' payee = fields.Many2One( 'party.party', "Payee", required=True, domain=[ ('id', 'in', Eval('payees', [])) ], context={ 'company': Eval('company', -1), }, depends=['company']) payees = fields.Many2Many( 'party.party', None, None, "Payees", readonly=True, context={ 'company': Eval('company', -1), }, depends=['company']) amount = Monetary( "Amount", currency='currency', digits='currency', required=True) currency = fields.Many2One('currency.currency', 'Currency', 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', -1)), ('debit_account', '!=', Eval('invoice_account', -1)), ('credit_account', '!=', Eval('invoice_account', -1)), ], depends={'amount'}) date = fields.Date('Date', required=True) @staticmethod def default_date(): Date = Pool().get('ir.date') return Date.today() class PayInvoiceAsk(ModelView): __name__ = 'account.invoice.pay.ask' type = fields.Selection([ ('writeoff', "Write-Off"), ('partial', "Partial Payment"), ('overpayment', "Overpayment"), ], 'Type', required=True, domain=[ If(Eval('amount_writeoff', 0) >= 0, ('type', 'in', ['writeoff', 'partial']), ()), ]) writeoff = fields.Many2One( 'account.move.reconcile.write_off', "Write Off", domain=[ ('company', '=', Eval('company', -1)), ], states={ 'invisible': Eval('type') != 'writeoff', 'required': Eval('type') == 'writeoff', }) amount = Monetary( "Payment Amount", currency='currency', digits='currency', readonly=True) currency = fields.Many2One('currency.currency', "Currency", readonly=True) amount_writeoff = Monetary( "Write-Off Amount", currency='currency', digits='currency', readonly=True, states={ 'invisible': ~Eval('type').in_(['writeoff', 'overpayment']), }) 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').in_(['writeoff', 'overpayment']), 'required': Eval('type').in_(['writeoff', 'overpayment']), }) payment_lines = fields.Many2Many('account.move.line', None, None, 'Payment Lines', readonly=True, states={ 'invisible': ~Eval('type').in_(['writeoff', 'overpayment']), }) company = fields.Many2One('company.company', 'Company', readonly=True) invoice = fields.Many2One('account.invoice', 'Invoice', readonly=True) @staticmethod def default_type(): return 'partial' @fields.depends( 'lines', 'amount', 'currency', 'invoice', 'payment_lines', 'company') def on_change_lines(self): self.amount_writeoff = Decimal(0) if not self.invoice: return def balance(line): if self.currency == line.second_currency: return line.amount_second_currency elif self.currency == self.company.currency: return line.debit - line.credit else: return 0 for line in self.lines: self.amount_writeoff += balance(line) for line in self.payment_lines: self.amount_writeoff += balance(line) if self.invoice.type == 'in': self.amount_writeoff = - self.amount_writeoff - self.amount else: self.amount_writeoff = self.amount_writeoff - self.amount class PayInvoice(Wizard): __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().__setup__() cls.__rpc__['create'].fresh_session = True def get_reconcile_lines_for_amount(self, invoice, amount, currency): if invoice.type == 'in': amount *= -1 return invoice.get_reconcile_lines_for_amount( amount, currency, party=self.start.payee) def default_start(self, fields): default = {} invoice = self.record payee = None if not invoice.alternative_payees: payee = invoice.party else: try: payee, = invoice.alternative_payees except ValueError: pass if payee: default['payee'] = payee.id default['payees'] = ( [invoice.party.id] + [p.id for p in invoice.alternative_payees]) default['company'] = invoice.company.id default['currency'] = invoice.currency.id default['amount'] = (invoice.amount_to_pay_today or invoice.amount_to_pay) default['invoice_account'] = invoice.account.id return default def transition_choice(self): invoice = self.record amount = self.start.amount currency = self.start.currency _, remainder = self.get_reconcile_lines_for_amount( invoice, amount, currency) if remainder == Decimal(0) and amount <= invoice.amount_to_pay: return 'pay' return 'ask' def default_ask(self, fields): default = {} invoice = self.record amount = self.start.amount currency = self.start.currency default['lines_to_pay'] = [x.id for x in invoice.lines_to_pay if not x.reconciliation] default['amount'] = amount default['currency'] = currency.id default['company'] = invoice.company.id if currency.is_zero(amount): lines = invoice.lines_to_pay else: lines, _ = self.get_reconcile_lines_for_amount( invoice, amount, currency) 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['invoice'] = invoice.id if amount >= invoice.amount_to_pay: default['type'] = 'overpayment' elif currency.is_zero(amount): default['type'] = 'writeoff' return default def transition_pay(self): pool = Pool() MoveLine = pool.get('account.move.line') Lang = pool.get('ir.lang') invoice = self.record amount = self.start.amount currency = self.start.currency reconcile_lines, remainder = ( self.get_reconcile_lines_for_amount(invoice, amount, currency)) overpayment = 0 if (0 <= invoice.amount_to_pay < amount or amount < 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: if not invoice.amount_to_pay: raise PayInvoiceError( gettext('account_invoice.msg_invoice_overpay_paid', invoice=invoice.rec_name)) overpayment = amount - invoice.amount_to_pay lines = [] if not currency.is_zero(amount): lines = invoice.pay_invoice( amount, self.start.payment_method, self.start.date, self.start.description, overpayment, party=self.start.payee) 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 and (not invoice.account.party_required or l.party == self.start.payee)) 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): __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'), }, help='If true, the current invoice(s) will be cancelled.') with_refund_allowed = fields.Boolean("With Refund Allowed", readonly=True) class CreditInvoice(Wizard): __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 class RescheduleLinesToPay(Wizard): __name__ = 'account.invoice.lines_to_pay.reschedule' start = StateAction('account.act_reschedule_lines_wizard') def do_start(self, action): return action, { 'ids': [ l.id for l in self.record.lines_to_pay if not l.reconciliation], 'model': 'account.move.line', } class DelegateLinesToPay(Wizard): __name__ = 'account.invoice.lines_to_pay.delegate' start = StateAction('account.act_delegate_lines_wizard') def do_start(self, action): return action, { 'ids': [ l.id for l in self.record.lines_to_pay if not l.reconciliation], 'model': 'account.move.line', }