diff --git a/modules/account_invoice/CHANGELOG b/modules/account_invoice/CHANGELOG index 07502653ee..7cadd8354b 100644 --- a/modules/account_invoice/CHANGELOG +++ b/modules/account_invoice/CHANGELOG @@ -1,3 +1,4 @@ +* Add payment means to invoices * Add an origin invoices field to the invoice * Remove default invoice type diff --git a/modules/account_invoice/doc/design.rst b/modules/account_invoice/doc/design.rst index 5f4dc2c50f..e0b383a3ec 100644 --- a/modules/account_invoice/doc/design.rst +++ b/modules/account_invoice/doc/design.rst @@ -182,9 +182,9 @@ remainder. Payment terms are create and managed from the main menu item: - |Financial --> Configuration --> Payment Terms --> Payment Terms|__ + |Financial --> Configuration --> Invoice Payments --> Payment Terms|__ - .. |Financial --> Configuration --> Payment Terms --> Payment Terms| replace:: :menuselection:`Financial --> Configuration --> Payment Terms --> Payment Terms` + .. |Financial --> Configuration --> Invoice Payments --> Payment Terms| replace:: :menuselection:`Financial --> Configuration --> Invoice Payments --> Payment Terms` __ https://demo.tryton.org/model/account.invoice.payment_term Wizards @@ -206,3 +206,24 @@ and amounts for each payment that will be required for that payment term. Payment terms can be tested out by opening the main menu item: :menuselection:`Financial --> Configuration --> Payment Terms --> Test Payment Term` + +.. _model-account.invoice.payment.mean.rule: + +Payment Mean Rule +================= + +The *Payment Mean Rule* stores the payment instruments that are applied +automatically on `Invoices ` without any of them on +validation or posting. +The selection is based on criteria such as the `company +` and the `currency +` of the invoice. + +.. seealso:: + + Payment means rules are created and managed from the main menu item: + + |Financial --> Configuration --> Invoice Payments --> Payment Means Rules|__ + + .. |Financial --> Configuration --> Invoice Payments --> Payment Means Rules| replace:: :menuselection:`Financial --> Configuration --> Invoice Payments --> Payment Means Rules` + __ https://demo.tryton.org/model/account.invoice.payment.mean.rule diff --git a/modules/account_invoice/invoice.fodt b/modules/account_invoice/invoice.fodt index 46b4005264..b2f0a2a807 100644 --- a/modules/account_invoice/invoice.fodt +++ b/modules/account_invoice/invoice.fodt @@ -2,17 +2,17 @@ - LibreOffice/25.2.5.2$Linux_X86_64 LibreOffice_project/520$Build-2 + LibreOffice/25.2.7.2$Linux_X86_64 LibreOffice_project/520$Build-2 - + - 205105 + 204893 0 25243 21883 @@ -21,12 +21,12 @@ view2 - 20849 - 217225 + 14400 + 217923 0 - 205105 + 204893 25241 - 226986 + 226774 0 1 false @@ -150,7 +150,7 @@ false true - 3274672 + 3380794 true false false @@ -628,12 +628,16 @@ + + + + + + + - - - @@ -664,7 +668,7 @@ </if> <choose> <when test="invoice.company.logo"> - + @@ -1087,14 +1091,20 @@ Reference <invoice.customer_payment_reference> </if> + <if test="invoice.payment_means"> + Payment Means + <for each="mean in invoice.payment_means"> + <mean.rec_name> + </for> + </if> </if> - <for each="comment in (invoice.comment or '').split('\n')"> + <for each="comment in (invoice.comment or '').split('\n')"> <comment> </for> - + \ No newline at end of file diff --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py index 0425a82b19..862beb7ed5 100644 --- a/modules/account_invoice/invoice.py +++ b/modules/account_invoice/invoice.py @@ -18,8 +18,8 @@ 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) + ChatMixin, DeactivableMixin, Index, MatchMixin, 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 @@ -27,7 +27,7 @@ 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.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval, Id, If from trytond.report import Report from trytond.rpc import RPC @@ -250,6 +250,9 @@ class Invoice( payment_term = fields.Many2One( 'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT', states=_states) + payment_means = fields.One2Many( + 'account.invoice.payment.mean', 'invoice', "Payment Means", + states=_states) alternative_payees = fields.Many2Many( 'account.invoice.alternative_payee', 'invoice', 'party', "Alternative Payee", states=_states, @@ -1485,6 +1488,26 @@ class Invoice( pattern.setdefault('country', self.invoice_address.country.id) return self.company.get_tax_identifier(pattern) + @classmethod + def set_payment_means(cls, invoices): + pool = Pool() + PaymentMean = pool.get('account.invoice.payment.mean') + PaymentMeanRule = pool.get('account.invoice.payment.mean.rule') + payment_means = [] + rules = PaymentMeanRule.search([]) + for invoice in invoices: + if invoice.type != 'out': + continue + if invoice.payment_means: + continue + pattern = PaymentMeanRule.pattern_from_invoice(invoice) + for rule in rules: + if rule.match(pattern): + payment_mean = PaymentMean.from_rule(rule) + payment_mean.invoice = invoice + payment_means.append(payment_mean) + PaymentMean.save(payment_means) + @property def invoice_report_versioned(self): return self.state in {'posted', 'paid'} and self.type == 'out' @@ -1560,6 +1583,14 @@ class Invoice( @classmethod def view_attributes(cls): + pool = Pool() + PaymentMean = pool.get('account.invoice.payment.mean') + if not PaymentMean.get_instruments(): + payment_means = [ + ('/form//field[@name="payment_means"]', 'invisible', '1'), + ] + else: + payment_means = [] return super().view_attributes() + [ ('/form//field[@name="comment"]', 'spell', Eval('party_lang')), ('/tree', 'visual', @@ -1570,7 +1601,15 @@ class Invoice( & (Eval('amount_to_pay_today', 0) < 0)), 'danger', If(Eval('state') == 'cancelled', 'muted', ''))), - ] + ] + payment_means + + @classmethod + def index_set_field(cls, name): + index = super().index_set_field(name) + if name == 'payment_means': + # payment means domain depends on alternative_payees + index = cls.index_set_field('alternative_payees') + 1 + return index @classmethod def check_modification(cls, mode, invoices, values=None, external=False): @@ -1974,6 +2013,7 @@ class Invoice( invoices_in = cls.browse([i for i in invoices if i.type == 'in']) cls.set_number(invoices_in) + cls.set_payment_means(invoices) cls._store_cache(invoices) moves = [] for invoice in invoices_in: @@ -2045,6 +2085,7 @@ class Invoice( context = transaction.context cls.set_number(invoices) + cls.set_payment_means(invoices) cls._store_cache(invoices) moves = [] for invoice in invoices: @@ -3396,6 +3437,178 @@ class InvoiceTax(sequence_ordered(), ModelSQL, ModelView): return line +class PaymentMean(ModelSQL, ModelView): + __name__ = 'account.invoice.payment.mean' + + invoice = fields.Many2One('account.invoice', "Invoice", required=True) + payees = fields.Function( + fields.Many2Many( + 'party.party', None, None, "Payees", + context={ + 'company': Eval('company', -1), + }), + 'on_change_with_payees') + payers = fields.Function( + fields.Many2Many( + 'party.party', None, None, "Payers", + context={ + 'company': Eval('company', -1), + }), + 'on_change_with_payers') + company = fields.Function( + fields.Many2One('company.company', "Company"), + 'on_change_with_company') + instrument = fields.Reference( + "Instrument", 'get_instruments', required=True) + + @classmethod + def __setup__(cls): + super().__setup__() + cls.__access__.add('invoice') + + @fields.depends( + 'invoice', '_parent_invoice.type', + '_parent_invoice.company', '_parent_invoice.party', + '_parent_invoice.alternative_payees') + def on_change_with_payees(self, name=None): + payees = [] + if self.invoice: + if self.invoice.type == 'out': + if self.invoice.company: + payees.append(self.invoice.company.party) + elif self.invoice.type == 'in': + if self.invoice.party: + payees.append(self.invoice.party) + if self.invoice.alternative_payees: + payees.extend(self.invoice.alternative_payees) + return payees + + @fields.depends( + 'invoice', '_parent_invoice.type', + '_parent_invoice.company', '_parent_invoice.party', + '_parent_invoice.alternative_payees') + def on_change_with_payers(self, name=None): + payers = [] + if self.invoice: + if self.invoice.type == 'in': + if self.invoice.company: + payers.append(self.invoice.company.party) + elif self.invoice.type == 'out': + if self.invoice.party: + payers.append(self.invoice.party) + if self.invoice.alternative_payees: + payers.extend(self.invoice.alternative_payees) + return payers + + @fields.depends('invoice', '_parent_invoice.company') + def on_change_with_company(self, name=None): + return self.invoice.company if self.invoice else None + + @classmethod + def _get_instruments(cls): + return [] + + @classmethod + def get_instruments(cls): + pool = Pool() + Model = pool.get('ir.model') + Rule = pool.get('account.invoice.payment.mean.rule') + get_name = Model.get_name + instruments = Rule.get_instruments() + for model in cls._get_instruments(): + instruments.append((model, get_name(model))) + return instruments + + def get_rec_name(self, name): + return self.instrument.rec_name + + @classmethod + def from_rule(cls, rule): + return cls(instrument=rule.instrument) + + +class PaymentMean_Bank(metaclass=PoolMeta): + __name__ = 'account.invoice.payment.mean' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.instrument.domain['bank.account'] = [ + ('owners.id', 'in', Eval('payees', [])), + ] + + def get_rec_name(self, name): + name = super().get_rec_name(name) + if self.instrument.__name__ == 'bank.account': + for number in self.instrument.numbers: + name = f'{number.type_string}: {number.number}' + break + else: + name = '' + return name + + +class PaymentMeanRule(sequence_ordered(), MatchMixin, ModelSQL, ModelView): + __name__ = 'account.invoice.payment.mean.rule' + + company = fields.Many2One('company.company', "Company", required=True) + currency = fields.Many2One('currency.currency', "Currency") + payee = fields.Function( + fields.Many2One( + 'party.party', "Payee", + context={ + 'company': Eval('company', -1), + }), + 'on_change_with_payee') + instrument = fields.Reference( + "Instrument", 'get_instruments', required=True) + + @classmethod + def default_company(cls): + return Transaction().context.get('company') + + @fields.depends('company') + def on_change_with_payee(self, name=None): + return self.company.party if self.company else None + + @classmethod + def _get_instruments(cls): + return [] + + @classmethod + def get_instruments(cls): + pool = Pool() + Model = pool.get('ir.model') + get_name = Model.get_name + instruments = [] + for model in cls._get_instruments(): + instruments.append((model, get_name(model))) + return instruments + + @classmethod + def pattern_from_invoice(cls, invoice): + return { + 'company': invoice.company.id, + 'currency': invoice.currency.id, + } + + +class PaymentMeanRule_Bank(metaclass=PoolMeta): + __name__ = 'account.invoice.payment.mean.rule' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.instrument.domain['bank.account'] = [ + ('owners.id', '=', Eval('payee', -1)), + ] + + @classmethod + def _get_instruments(cls): + yield from super()._get_instruments() + yield 'bank.account' + + class PaymentMethod(DeactivableMixin, ModelSQL, ModelView): __name__ = 'account.invoice.payment.method' company = fields.Many2One('company.company', "Company", required=True) diff --git a/modules/account_invoice/invoice.xml b/modules/account_invoice/invoice.xml index e802f104f2..ac8b664551 100644 --- a/modules/account_invoice/invoice.xml +++ b/modules/account_invoice/invoice.xml @@ -9,6 +9,12 @@ this repository contains the full copyright notices and license terms. --> sequence="20" id="menu_invoices"/> + + Pay Invoice account.invoice.pay @@ -397,6 +403,80 @@ this repository contains the full copyright notices and license terms. --> invoice_tax_tree_sequence + + account.invoice.payment.mean + form + invoice_payment_mean_form + + + + account.invoice.payment.mean + tree + invoice_payment_mean_list + + + + account.invoice.payment.mean.rule + form + invoice_payment_mean_rule_form + + + + account.invoice.payment.mean.rule + tree + invoice_payment_mean_rule_list + + + + Payment Means Rules + account.invoice.payment.mean.rule + + + + + + + + + + + + + + + account.invoice.payment.mean.rule + + + + + + + + account.invoice.payment.mean.rule + + + + + + + + + User in companies + account.invoice.payment.mean.rule + + + + + + + account.invoice.pay.start form diff --git a/modules/account_invoice/payment_term.xml b/modules/account_invoice/payment_term.xml index 3c1e8dd92f..51ffb3b19a 100644 --- a/modules/account_invoice/payment_term.xml +++ b/modules/account_invoice/payment_term.xml @@ -3,11 +3,6 @@ this repository contains the full copyright notices and license terms. --> - account.invoice.payment_term form @@ -33,7 +28,7 @@ this repository contains the full copyright notices and license terms. --> @@ -106,7 +101,7 @@ this repository contains the full copyright notices and license terms. --> diff --git a/modules/account_invoice/setup.py b/modules/account_invoice/setup.py index 8267f0c5e3..6aae2e55fc 100755 --- a/modules/account_invoice/setup.py +++ b/modules/account_invoice/setup.py @@ -50,7 +50,10 @@ for dep in info.get('depends', []): requires.append(get_require_version('trytond_%s' % dep)) requires.append(get_require_version('trytond')) -tests_require = [get_require_version('proteus')] +tests_require = [ + get_require_version('proteus'), + get_require_version('trytond_bank'), + ] setup(name=name, version=version, diff --git a/modules/account_invoice/tests/scenario_invoice_payment_means.rst b/modules/account_invoice/tests/scenario_invoice_payment_means.rst new file mode 100644 index 0000000000..a6cdf2416f --- /dev/null +++ b/modules/account_invoice/tests/scenario_invoice_payment_means.rst @@ -0,0 +1,71 @@ +============================== +Invoice Payment Means Scenario +============================== + +Imports:: + + >>> from proteus import Model + >>> from trytond.modules.account.tests.tools import ( + ... create_chart, create_fiscalyear, get_accounts) + >>> from trytond.modules.account_invoice.tests.tools import ( + ... set_fiscalyear_invoice_sequences) + >>> from trytond.modules.company.tests.tools import create_company, get_company + >>> from trytond.tests.tools import activate_modules, assertEqual, assertFalse + +Activate modules:: + + >>> config = activate_modules( + ... ['account_invoice', 'bank'], create_company, create_chart) + + >>> BankAccount = Model.get('bank.account') + >>> Invoice = Model.get('account.invoice') + >>> Party = Model.get('party.party') + >>> PaymentMeanRule = Model.get('account.invoice.payment.mean.rule') + + >>> company = get_company() + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences(create_fiscalyear()) + >>> fiscalyear.click('create_period') + +Get accounts:: + + >>> accounts = get_accounts() + +Create party:: + + >>> customer = Party(name='Party') + >>> customer.save() + +Setup payment mean rules:: + + >>> bank_account1 = BankAccount(owners=[Party(company.party.id)]) + >>> _ = bank_account1.numbers.new(type='other', number='12345') + >>> bank_account1.save() + + >>> bank_account2 = BankAccount(owners=[Party(company.party.id)]) + >>> _ = bank_account1.numbers.new(type='other', number='67890') + >>> bank_account2.save() + + >>> payment_mean_rule = PaymentMeanRule(instrument=bank_account1) + >>> payment_mean_rule.save() + +Create invoice without payment means:: + + >>> invoice = Invoice(type='out') + >>> invoice.party = customer + >>> assertFalse(invoice.payment_means) + >>> invoice.click('validate_invoice') + >>> payment_mean, = invoice.payment_means + >>> assertEqual(payment_mean.instrument, bank_account1) + +Create invoice with payment means:: + + >>> invoice = Invoice(type='out') + >>> invoice.party = customer + >>> payment_mean = invoice.payment_means.new() + >>> payment_mean.instrument = bank_account2 + >>> invoice.click('post') + >>> payment_mean, = invoice.payment_means + >>> assertEqual(payment_mean.instrument, bank_account2) diff --git a/modules/account_invoice/tests/test_module.py b/modules/account_invoice/tests/test_module.py index 647718dfb6..840525616b 100644 --- a/modules/account_invoice/tests/test_module.py +++ b/modules/account_invoice/tests/test_module.py @@ -40,6 +40,7 @@ class AccountInvoiceTestCase( ModuleTestCase): 'Test AccountInvoice module' module = 'account_invoice' + extras = ['bank'] @with_transaction() def test_payment_term(self): diff --git a/modules/account_invoice/tryton.cfg b/modules/account_invoice/tryton.cfg index 2e1fb1e9a5..b28abfd3c3 100644 --- a/modules/account_invoice/tryton.cfg +++ b/modules/account_invoice/tryton.cfg @@ -9,6 +9,8 @@ depends: party product res +extras_depend: + bank xml: invoice.xml payment_term.xml @@ -31,6 +33,8 @@ model: invoice.InvoiceLine invoice.InvoiceLineTax invoice.InvoiceTax + invoice.PaymentMean + invoice.PaymentMeanRule invoice.InvoiceEdocumentStart invoice.InvoiceEdocumentResult invoice.PayInvoiceStart @@ -70,3 +74,8 @@ wizard: account.CancelMoves report: invoice.InvoiceReport + +[register bank] +model: + invoice.PaymentMean_Bank + invoice.PaymentMeanRule_Bank diff --git a/modules/account_invoice/view/invoice_form.xml b/modules/account_invoice/view/invoice_form.xml index 9c073ae470..d01547e2f9 100644 --- a/modules/account_invoice/view/invoice_form.xml +++ b/modules/account_invoice/view/invoice_form.xml @@ -83,7 +83,8 @@ this repository contains the full copyright notices and license terms. -->