From 3a4a23c3df3e53ebf6e32eeef2127229f5b063fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 30 Sep 2025 00:41:08 +0200 Subject: [PATCH] Support Belgian structured communication on invoices, payments and statement rules --- modules/account_be/CHANGELOG | 1 + modules/account_be/account.py | 77 +++++++++++++++++++++++++ modules/account_be/ogm_vcs.py | 49 ++++++++++++++++ modules/account_be/setup.py | 11 +++- modules/account_be/tests/test_module.py | 1 + modules/account_be/tryton.cfg | 15 +++++ 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 modules/account_be/ogm_vcs.py diff --git a/modules/account_be/CHANGELOG b/modules/account_be/CHANGELOG index 6aca66c4bd..58d14ecfa9 100644 --- a/modules/account_be/CHANGELOG +++ b/modules/account_be/CHANGELOG @@ -1,3 +1,4 @@ +* Support structured communication on invoices, payments and statement rules Version 7.6.0 - 2025-04-28 -------------------------- diff --git a/modules/account_be/account.py b/modules/account_be/account.py index a9068860ee..97354b752e 100644 --- a/modules/account_be/account.py +++ b/modules/account_be/account.py @@ -1,7 +1,13 @@ # 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 stdnum.exceptions from sql.aggregate import Min, Sum +try: + from stdnum.be import ogm_vcs +except ImportError: + from . import ogm_vcs + from trytond.model import ModelSQL, ModelView, fields from trytond.modules.account.exceptions import FiscalYearNotFoundError from trytond.modules.currency.fields import Monetary @@ -138,3 +144,74 @@ class BEVATCustomerContext(ModelView): return None return fiscalyear.id return context['fiscalyear'] + + +class Invoice(metaclass=PoolMeta): + __name__ = 'account.invoice' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.supplier_payment_reference_type.selection.append( + ('ogm_vcs', "Belgian structured communication")) + + @classmethod + def _format_supplier_payment_reference_ogm_vcs(cls, reference): + try: + return ogm_vcs.format(reference) + except stdnum.exceptions.ValidationError: + return reference + + @classmethod + def _search_customer_payment_reference(cls, value): + yield from super()._search_customer_payment_reference(value) + if ogm_vcs.is_valid(value): + yield ('number_digit', '=', int(ogm_vcs.compact(value)[:-2])) + + def _customer_payment_reference_type(self): + type = super()._customer_payment_reference_type() + if (self.invoice_address.country + and self.invoice_address.country.code == 'BE'): + type = 'ogm_vcs' + return type + + def _format_customer_payment_reference_ogm_vcs(self, source): + number = self._customer_payment_reference_number_digit(source) + if number: + number = number % 10**10 + check_digit = ogm_vcs.calc_check_digit(str(number)) + reference = f'{self.number_digit:0>10}{check_digit}' + return f'+++{ogm_vcs.format(reference)}+++' + + def _check_supplier_payment_reference_ogm_vcs(self): + return ogm_vcs.is_valid(self.supplier_payment_reference) + + +class Payment(metaclass=PoolMeta): + __name__ = 'account.payment' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.reference_type.selection.append( + ('ogm_vcs', "Belgian structured communication")) + + @classmethod + def _format_reference_ogm_vcs(cls, reference): + try: + return ogm_vcs.format(reference) + except stdnum.exceptions.ValidationError: + return reference + + def _check_reference_ogm_vcs(self): + return ogm_vcs.is_valid(self.reference) + + +class StatementRuleLine(metaclass=PoolMeta): + __name__ = 'account.statement.rule.line' + + @classmethod + def _party_payment_reference(cls, value): + yield from super()._party_payment_reference(value) + if ogm_vcs.is_valid(value): + yield ('code_digit', '=', int(ogm_vcs.compact(value)[:-2])) diff --git a/modules/account_be/ogm_vcs.py b/modules/account_be/ogm_vcs.py new file mode 100644 index 0000000000..c79ec3c73f --- /dev/null +++ b/modules/account_be/ogm_vcs.py @@ -0,0 +1,49 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +from stdnum.exceptions import ( + InvalidChecksum, InvalidFormat, InvalidLength, ValidationError) +from stdnum.util import clean, isdigits + + +def compact(number: str) -> str: + """Convert the number to the minimal representation. This strips the number + of any invalid separators and removes surrounding whitespace.""" + return clean(number, ' +/').strip() + + +def checksum(number: str) -> int: + """Calculate the checksum.""" + return (int(number) % 97) or 97 + + +def calc_check_digit(number: str) -> str: + """Calculate the check digit that should be added.""" + return '%02d' % checksum(number) + + +def validate(number: str) -> str: + """Check if the number is a valid OGM-VCS.""" + number = compact(number) + if not isdigits(number) or int(number) <= 0: + raise InvalidFormat() + if len(number) != 12: + raise InvalidLength() + if checksum(number[:-2]) != int(number[-2:]): + raise InvalidChecksum() + return number + + +def is_valid(number: str) -> bool: + """Check if the number is a valid VAT number.""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number: str) -> str: + """Format the number provided for output.""" + number = compact(number) + number = number.rjust(12, '0') + return f'{number[:3]}/{number[3:7]}/{number[7:]}' diff --git a/modules/account_be/setup.py b/modules/account_be/setup.py index 481fd8ae7a..641f9fa017 100755 --- a/modules/account_be/setup.py +++ b/modules/account_be/setup.py @@ -44,12 +44,18 @@ else: download_url = 'http://downloads.tryton.org/%s.%s/' % ( major_version, minor_version) -requires = [] +requires = ['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)) requires.append(get_require_version('trytond')) +tests_require = [ + get_require_version('trytond_account_invoice'), + get_require_version('trytond_account_payment'), + get_require_version('trytond_account_statement_rule'), + ] + setup(name=name, version=version, description='Tryton module with Belgian chart of accounts', @@ -99,6 +105,9 @@ setup(name=name, license='GPL-3', python_requires='>=3.9', install_requires=requires, + extras_require={ + 'test': tests_require, + }, zip_safe=False, entry_points=""" [trytond.modules] diff --git a/modules/account_be/tests/test_module.py b/modules/account_be/tests/test_module.py index 1311e1d410..a0aaeee63d 100644 --- a/modules/account_be/tests/test_module.py +++ b/modules/account_be/tests/test_module.py @@ -9,6 +9,7 @@ from trytond.tests.test_tryton import ModuleTestCase, with_transaction class AccountBETestCase(ModuleTestCase): 'Test Account BE module' module = 'account_be' + extras = ['account_invoice', 'account_payment', 'account_statement_rule'] language = 'fr' @with_transaction() diff --git a/modules/account_be/tryton.cfg b/modules/account_be/tryton.cfg index 3f6e848421..23b491271f 100644 --- a/modules/account_be/tryton.cfg +++ b/modules/account_be/tryton.cfg @@ -7,6 +7,9 @@ depends: extras_depend: account_asset account_deposit + account_invoice + account_payment + account_statement_rule sale_advance_payment xml: account.xml @@ -21,3 +24,15 @@ model: account.BEVATCustomerContext wizard: account.CreateChart + +[register account_invoice] +model: + account.Invoice + +[register account_payment] +model: + account.Payment + +[register account_statement_rule] +model: + account.StatementRuleLine