diff --git a/modules/account_payment/CHANGELOG b/modules/account_payment/CHANGELOG index f86e78ba1f..f92f3ea838 100644 --- a/modules/account_payment/CHANGELOG +++ b/modules/account_payment/CHANGELOG @@ -1,3 +1,4 @@ +* Validate the format of payment references Version 7.6.0 - 2025-04-28 -------------------------- diff --git a/modules/account_payment/message.xml b/modules/account_payment/message.xml index baae9f3464..afc1b836bb 100644 --- a/modules/account_payment/message.xml +++ b/modules/account_payment/message.xml @@ -12,6 +12,9 @@ this repository contains the full copyright notices and license terms. --> The line "%(line)s" of payment "%(payment)s" is already reconciled. + + The %(type)s "%(reference)s" on payment "%(payment)s" is not valid. + You cannot erase party "%(party)s" while they have pending payments with company "%(company)s". diff --git a/modules/account_payment/payment.py b/modules/account_payment/payment.py index a3ca2a05af..bb3abb17ec 100644 --- a/modules/account_payment/payment.py +++ b/modules/account_payment/payment.py @@ -4,9 +4,11 @@ from collections import defaultdict from decimal import Decimal from itertools import groupby +import stdnum.exceptions from sql.aggregate import Count, Sum from sql.functions import CharLength from sql.operators import Abs +from stdnum import iso11649 from trytond import backend from trytond.i18n import gettext @@ -16,7 +18,7 @@ from trytond.model.exceptions import AccessError from trytond.modules.company.model import ( employee_field, reset_employee, set_employee) from trytond.modules.currency.fields import Monetary -from trytond.pool import Pool +from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval, If from trytond.rpc import RPC from trytond.tools import ( @@ -25,7 +27,8 @@ from trytond.tools import ( from trytond.transaction import Transaction from trytond.wizard import StateAction, Wizard -from .exceptions import OverpayWarning, ReconciledWarning +from .exceptions import ( + OverpayWarning, PaymentValidationError, ReconciledWarning) KINDS = [ ('payable', 'Payable'), @@ -276,6 +279,11 @@ class Payment(Workflow, ModelSQL, ModelView): _rec_name = 'number' number = fields.Char("Number", required=True, readonly=True) reference = fields.Char("Reference", states=_STATES) + reference_type = fields.Selection([ + (None, ""), + ('creditor_reference', "Creditor Reference"), + ], "Reference Type") + reference_type_string = reference_type.translated('reference_type') company = fields.Many2One( 'company.company', "Company", required=True, states=_STATES) journal = fields.Many2One('account.payment.journal', 'Journal', @@ -489,6 +497,19 @@ class Payment(Workflow, ModelSQL, ModelView): table, _ = tables[None] return [CharLength(table.number), table.number] + @fields.depends('reference_type', 'reference') + def on_change_with_reference(self): + if (reference := self.reference) and (type := self.reference_type): + reference = getattr(self, f'_format_reference_{type}')(reference) + return reference + + @classmethod + def _format_reference_creditor_reference(cls, reference): + try: + return iso11649.format(reference) + except stdnum.exceptions.ValidationError: + return reference + @staticmethod def default_company(): return Transaction().context.get('company') @@ -603,6 +624,7 @@ class Payment(Workflow, ModelSQL, ModelView): default = default.copy() default.setdefault('number', None) default.setdefault('reference') + default.setdefault('reference_type') default.setdefault('group', None) default.setdefault('approved_by') default.setdefault('succeeded_by') @@ -663,6 +685,31 @@ class Payment(Workflow, ModelSQL, ModelView): callback.append(lambda: Line.set_payment_amount(lines)) return callback + @classmethod + def validate_fields(cls, payments, field_names): + super().validate_fields(payments, field_names) + cls.check_reference(payments, field_names) + + @classmethod + def check_reference(cls, payments, field_names): + if field_names and not (field_names & {'reference', 'reference_type'}): + return + + for payment in payments: + if type := payment.reference_type: + method = getattr(cls, f'_check_reference_{type}') + if not method(payment): + reference = payment.reference + type = payment.reference_type_string + raise PaymentValidationError(gettext( + 'account_payment.msg_payment_reference_invalid', + type=type, + reference=reference, + payment=payment.rec_name)) + + def _check_reference_creditor_reference(self): + return iso11649.is_valid(self.reference) + @classmethod @ModelView.button @Workflow.transition('draft') diff --git a/modules/account_payment/tests/scenario_account_payment.rst b/modules/account_payment/tests/scenario_account_payment.rst index 8afc21cfdd..e300fc33eb 100644 --- a/modules/account_payment/tests/scenario_account_payment.rst +++ b/modules/account_payment/tests/scenario_account_payment.rst @@ -90,6 +90,10 @@ Partially pay line:: >>> payment.amount Decimal('50.00') >>> payment.amount = Decimal('20.00') + >>> payment.reference_type = 'creditor_reference' + >>> payment.reference = 'RF18539007547034' + >>> payment.reference + 'RF18 5390 0754 7034' >>> payment.click('submit') >>> assertEqual(payment.submitted_by, employee) >>> payment.click('approve') diff --git a/modules/account_payment/view/payment_form.xml b/modules/account_payment/view/payment_form.xml index ba578dd511..4a79ee04bd 100644 --- a/modules/account_payment/view/payment_form.xml +++ b/modules/account_payment/view/payment_form.xml @@ -22,7 +22,9 @@ this repository contains the full copyright notices and license terms. -->