From e967e80974233f7ea90029c26312a5142dbd5bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 30 Sep 2025 00:11:08 +0200 Subject: [PATCH] Add payment reference to statement rule --- modules/account_invoice/invoice.py | 16 +++- modules/account_statement_rule/CHANGELOG | 1 + modules/account_statement_rule/account.py | 84 ++++++++++++++--- modules/account_statement_rule/setup.py | 2 +- ...atement_rule_keyword_payment_reference.rst | 93 +++++++++++++++++++ ...ule_keyword_payment_reference_customer.rst | 70 ++++++++++++++ 6 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference.rst create mode 100644 modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference_customer.rst diff --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py index e654b45dd4..bf7d348d24 100644 --- a/modules/account_invoice/invoice.py +++ b/modules/account_invoice/invoice.py @@ -167,7 +167,8 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin): states={ 'invisible': Eval('type', 'out') != 'out', }), - 'get_customer_payment_reference') + 'get_customer_payment_reference', + searcher='search_customer_payment_reference') sequence = fields.Integer("Sequence", readonly=True) sequence_type_cache = fields.Selection([ (None, ""), @@ -656,6 +657,19 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin): 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' diff --git a/modules/account_statement_rule/CHANGELOG b/modules/account_statement_rule/CHANGELOG index 8abfbc8bc6..9d0e338629 100644 --- a/modules/account_statement_rule/CHANGELOG +++ b/modules/account_statement_rule/CHANGELOG @@ -1,3 +1,4 @@ +Add payment reference to statement rule Version 7.6.0 - 2025-04-28 -------------------------- diff --git a/modules/account_statement_rule/account.py b/modules/account_statement_rule/account.py index 9cdc745c08..2754affe4c 100644 --- a/modules/account_statement_rule/account.py +++ b/modules/account_statement_rule/account.py @@ -6,6 +6,7 @@ import re from decimal import Decimal from simpleeval import simple_eval +from stdnum import iso11649 from trytond.model import ModelSQL, ModelView, fields, sequence_ordered from trytond.modules.currency.fields import Monetary @@ -95,7 +96,8 @@ class StatementRule(sequence_ordered(), ModelSQL, ModelView): "It may define the groups named:\n" "'party'\n" "'bank_account'\n" - "'invoice'") + "'invoice'\n" + "'payment_reference'") information_rules = fields.One2Many( 'account.statement.rule.information', 'rule', "Information Rules") @@ -322,7 +324,7 @@ class StatementRuleLine(sequence_ordered(), ModelSQL, ModelView): currency = origin.statement.journal.currency amount = currency.round(simple_eval(decistmt(self.amount), **context)) - party = self._get_party(origin, keywords) + party = self._get_party(origin, keywords, amount=amount) related_to = list( filter(None, self._get_related_to( origin, keywords, party=party, amount=amount))) @@ -370,15 +372,21 @@ class StatementRuleLine(sequence_ordered(), ModelSQL, ModelView): line.related_to = related_to return line - def _get_party(self, origin, keywords): + def _get_party(self, origin, keywords, amount=0): pool = Pool() Party = pool.get('party.party') Line = pool.get('account.statement.line') + Configuration = pool.get('account.configuration') try: AccountNumber = pool.get('bank.account.number') except KeyError: AccountNumber = None + configuration = Configuration(1) + customer_payment_reference_number = configuration.get_multivalue( + 'customer_payment_reference_number', + company=origin.company.id) + party = self.party if not party: if keywords.get('bank_account') and AccountNumber: @@ -402,15 +410,29 @@ class StatementRuleLine(sequence_ordered(), ModelSQL, ModelView): if lines: line, = lines party = line.party - elif keywords.get('party'): - parties = Party.search( - [('rec_name', 'ilike', keywords['party'])]) + elif (keywords.get('party') + or (keywords.get('payment_reference') + and amount > 0 + and customer_payment_reference_number == 'party')): + domain = [] + if party_rec_name := keywords.get('party'): + domain.append(('rec_name', 'ilike', party_rec_name)) + if ((payment_reference := keywords.get('payment_reference')) + and amount > 0 + and customer_payment_reference_number == 'party'): + domain.extend( + self._party_payment_reference(payment_reference)) + if domain: + domain.insert(0, 'OR') + parties = Party.search(domain) + else: + parties = [] if len(parties) == 1: party, = parties - else: + elif party_rec_name: lines = Line.search([ ('statement.state', 'in', ['validated', 'posted']), - ('origin.keywords.party', '=', keywords['party']), + ('origin.keywords.party', '=', party_rec_name), ('party', '!=', None), ], order=[('date', 'DESC')], limit=1) @@ -419,8 +441,14 @@ class StatementRuleLine(sequence_ordered(), ModelSQL, ModelView): party = line.party return party + @classmethod + def _party_payment_reference(cls, value): + if iso11649.is_valid(value): + yield ('code_alnum', '=', value[4:]) + def _get_related_to(self, origin, keywords, party=None, amount=0): - return {self._get_invoice(origin, keywords, party=party)} + return { + self._get_invoice(origin, keywords, party=party, amount=amount)} def _get_party_from(self, related_to): pool = Pool() @@ -434,16 +462,48 @@ class StatementRuleLine(sequence_ordered(), ModelSQL, ModelView): if isinstance(related_to, Invoice): return related_to.account - def _get_invoice(self, origin, keywords, party=None): + def _get_invoice(self, origin, keywords, party=None, amount=0): pool = Pool() Invoice = pool.get('account.invoice') - if keywords.get('invoice'): + Configuration = pool.get('account.configuration') + + configuration = Configuration(1) + customer_payment_reference_number = configuration.get_multivalue( + 'customer_payment_reference_number', + company=origin.company.id) + + if keywords.get('invoice') or keywords.get('payment_reference'): + kdomain = ['OR'] + if invoice_rec_name := keywords.get('invoice'): + kdomain.append(('rec_name', '=', invoice_rec_name)) + if payment_reference := keywords.get('payment_reference'): + if (amount > 0 + and customer_payment_reference_number == 'invoice'): + kdomain.append( + ('customer_payment_reference', '=', payment_reference)) + elif amount < 0: + kdomain.append( + ('supplier_payment_reference', '=', payment_reference)) domain = [ - ('rec_name', '=', keywords['invoice']), + kdomain, ('company', '=', origin.company.id), ('currency', '=', origin.currency.id), ('state', '=', 'posted'), ] + if amount > 0: + domain.append(['OR', + [('type', '=', 'out'), + ('total_amount', '>=', 0)], + [('type', '=', 'in'), + ('total_amount', '<=', 0)], + ]) + elif amount < 0: + domain.append(['OR', + [('type', '=', 'out'), + ('total_amount', '<=', 0)], + [('type', '=', 'in'), + ('total_amount', '>=', 0)], + ]) if party: domain.append(['OR', ('party', '=', party.id), diff --git a/modules/account_statement_rule/setup.py b/modules/account_statement_rule/setup.py index 7cf4d09988..4ef87c9f36 100755 --- a/modules/account_statement_rule/setup.py +++ b/modules/account_statement_rule/setup.py @@ -44,7 +44,7 @@ else: download_url = 'http://downloads.tryton.org/%s.%s/' % ( major_version, minor_version) -requires = ['simpleeval'] +requires = ['simpleeval', 'python-stdnum'] for dep in info.get('depends', []): if not re.match(r'(ir|res)(\W|$)', dep): requires.append(get_require_version('trytond_%s' % dep)) diff --git a/modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference.rst b/modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference.rst new file mode 100644 index 0000000000..c8832b0765 --- /dev/null +++ b/modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference.rst @@ -0,0 +1,93 @@ +========================================================= +Account Statement Rule Keyword Payment Reference Scenario +========================================================== + +Imports:: + + >>> import datetime as dt + >>> from decimal import Decimal + + >>> 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 + >>> from trytond.tests.tools import activate_modules, assertEqual + + >>> today = dt.date.today() + +Activate modules:: + + >>> config = activate_modules( + ... ['account_statement_rule'], + ... create_company, create_chart) + + >>> AccountConfiguration = Model.get('account.configuration') + >>> AccountJournal = Model.get('account.journal') + >>> Invoice = Model.get('account.invoice') + >>> Party = Model.get('party.party') + >>> Rule = Model.get('account.statement.rule') + >>> Statement = Model.get('account.statement') + >>> StatementJournal = Model.get('account.statement.journal') + +Create fiscal year:: + + >>> fiscalyear = set_fiscalyear_invoice_sequences( + ... create_fiscalyear(today=today)) + >>> fiscalyear.click('create_period') + +Get accounts:: + + >>> accounts = get_accounts() + + >>> account_configuration = AccountConfiguration(1) + >>> account_configuration.customer_payment_reference_number = 'invoice' + >>> account_configuration.save() + +Create a party:: + + >>> customer = Party(name="Customer", code="CUST-001") + >>> customer.save() + +Create an invoice:: + + >>> invoice = Invoice(party=customer) + >>> line = invoice.lines.new() + >>> line.quantity = 1 + >>> line.unit_price = Decimal('40.0000') + >>> line.account = accounts['revenue'] + >>> invoice.click('post') + >>> invoice.state + 'posted' + +Create statement rules:: + + >>> rule = Rule(name="Payment Reference Rule") + >>> rule.description = r"Ref: *(?P.*)" + >>> line = rule.lines.new() + >>> line.amount = "pending" + >>> rule.save() + +Create a statement:: + + >>> account_journal, = AccountJournal.find([('code', '=', 'STA')], limit=1) + >>> journal_number = StatementJournal( + ... name="Number", + ... journal=account_journal, + ... account=accounts['cash'], + ... validation='number_of_lines') + >>> journal_number.save() + + >>> statement = Statement(name="Test") + >>> statement.journal = journal_number + >>> statement.number_of_lines = 1 + >>> origin = statement.origins.new() + >>> origin.date = today + >>> origin.amount = Decimal('50.00') + >>> origin.description = "Ref: " + invoice.customer_payment_reference + >>> statement.click('apply_rules') + >>> line, = statement.lines + >>> assertEqual(line.related_to, invoice) + >>> assertEqual(line.party, customer) + >>> assertEqual(line.account, invoice.account) diff --git a/modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference_customer.rst b/modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference_customer.rst new file mode 100644 index 0000000000..c964849824 --- /dev/null +++ b/modules/account_statement_rule/tests/scenario_account_statement_rule_keyword_payment_reference_customer.rst @@ -0,0 +1,70 @@ +================================================================== +Account Statement Rule Keyword Payment Reference Customer Scenario +================================================================== + +Imports:: + + >>> import datetime as dt + >>> from decimal import Decimal + + >>> from proteus import Model + >>> from trytond.modules.account.tests.tools import create_chart, get_accounts + >>> from trytond.modules.company.tests.tools import create_company + >>> from trytond.tests.tools import activate_modules, assertEqual + + >>> today = dt.date.today() + +Activate modules:: + + >>> config = activate_modules( + ... ['account_statement_rule'], + ... create_company, create_chart) + + >>> AccountConfiguration = Model.get('account.configuration') + >>> AccountJournal = Model.get('account.journal') + >>> Party = Model.get('party.party') + >>> Rule = Model.get('account.statement.rule') + >>> Statement = Model.get('account.statement') + >>> StatementJournal = Model.get('account.statement.journal') + +Get accounts:: + + >>> accounts = get_accounts() + + >>> account_configuration = AccountConfiguration(1) + >>> account_configuration.customer_payment_reference_number = 'party' + >>> account_configuration.save() + +Create a party:: + + >>> customer = Party(name="Customer", code="CUST-001") + >>> customer.save() + +Create statement rules:: + + >>> rule = Rule(name="Payment Reference Rule") + >>> rule.description = r"Ref: *(?P.*)" + >>> line = rule.lines.new() + >>> line.amount = "pending" + >>> rule.save() + +Create a statement:: + + >>> account_journal, = AccountJournal.find([('code', '=', 'STA')], limit=1) + >>> journal_number = StatementJournal( + ... name="Number", + ... journal=account_journal, + ... account=accounts['cash'], + ... validation='number_of_lines') + >>> journal_number.save() + + >>> statement = Statement(name="Test") + >>> statement.journal = journal_number + >>> statement.number_of_lines = 1 + >>> origin = statement.origins.new() + >>> origin.date = today + >>> origin.amount = Decimal('50.00') + >>> origin.description = "Ref: RF92CUST001" + >>> statement.click('apply_rules') + >>> line, = statement.lines + >>> assertEqual(line.party, customer)