details:   https://code.tryton.org/tryton/commit/dccdca1b6902
branch:    default
user:      Cédric Krier <[email protected]>
date:      Mon Feb 16 10:49:41 2026 +0100
description:
        Fill payment means when parsing UBL invoice
diffstat:

 modules/edocument_ubl/CHANGELOG                              |    1 +
 modules/edocument_ubl/edocument.py                           |  102 +++++++++++
 modules/edocument_ubl/setup.py                               |    3 +-
 modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst |    8 +-
 modules/edocument_ubl/tryton.cfg                             |   10 +
 5 files changed, 122 insertions(+), 2 deletions(-)

diffs (248 lines):

diff -r dc28a27b9e81 -r dccdca1b6902 modules/edocument_ubl/CHANGELOG
--- a/modules/edocument_ubl/CHANGELOG   Mon Feb 16 13:18:22 2026 +0100
+++ b/modules/edocument_ubl/CHANGELOG   Mon Feb 16 10:49:41 2026 +0100
@@ -1,3 +1,4 @@
+* Fill payment means
 * Render payment means
 * Set BillingReference to Invoice and Credit Note
 * Render allowance and charge
diff -r dc28a27b9e81 -r dccdca1b6902 modules/edocument_ubl/edocument.py
--- a/modules/edocument_ubl/edocument.py        Mon Feb 16 13:18:22 2026 +0100
+++ b/modules/edocument_ubl/edocument.py        Mon Feb 16 10:49:41 2026 +0100
@@ -17,6 +17,7 @@
 # XXX fix: https://genshi.edgewall.org/ticket/582
 from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
 from lxml import etree
+from stdnum import bic, iban
 
 from trytond.i18n import gettext, ngettext
 from trytond.model import Model
@@ -301,6 +302,7 @@
             root.findtext('./{*}IssueDate'))
         invoice.party = cls._parse_2_supplier(
             root.find('./{*}AccountingSupplierParty'), create=True)
+        payees = [invoice.party]
         invoice.set_journal()
         invoice.on_change_party()
         invoice.invoice_address = cls._parse_2_address(
@@ -333,6 +335,7 @@
                 party = cls._create_2_party(payee_party)
                 party.save()
             invoice.alternative_payees = [party]
+            payees.append(party)
 
         currency_code = root.findtext('./{*}DocumentCurrencyCode')
         if not currency_code:
@@ -349,6 +352,10 @@
                         'edocument_ubl.msg_currency_not_found',
                         code=currency_code))
 
+        invoice.supplier_payment_reference = root.findtext(
+            './{*}PaymentMeans/{*}PaymentID')
+        invoice.payment_means = cls._parse_2_payment_means(
+            root.findall('./{*}PaymentMeans'), payees=payees)
         invoice.payment_term_date = cls._parse_2_payment_term_date(
             root.findall('./{*}PaymentTerms'))
         lines = [
@@ -590,6 +597,7 @@
             root.findtext('./{*}IssueDate'))
         invoice.party = cls._parse_2_supplier(
             root.find('./{*}AccountingSupplierParty'), create=True)
+        payees = [invoice.party]
         invoice.set_journal()
         invoice.on_change_party()
         if (seller := root.find('./{*}SellerSupplierParty')) is not None:
@@ -607,6 +615,7 @@
                 party = cls._create_2_party(payee_party)
                 party.save()
             invoice.alternative_payees = [party]
+            payees.append(party)
         if (currency_code := root.findtext('./{*}DocumentCurrencyCode')
                 ) is not None:
             try:
@@ -617,6 +626,11 @@
                 raise InvoiceError(gettext(
                         'edocument_ubl.msg_currency_not_found',
                         code=currency_code))
+
+        invoice.supplier_payment_reference = root.findtext(
+            './{*}PaymentMeans/{*}PaymentID')
+        invoice.payment_means = cls._parse_2_payment_means(
+            root.findall('./{*}PaymentMeans'), payees=payees)
         invoice.payment_term_date = cls._parse_2_payment_term_date(
             root.findall('./{*}PaymentTerms'))
         lines = [
@@ -1060,6 +1074,20 @@
                 return company
 
     @classmethod
+    def _parse_2_payment_means(cls, payment_means, payees):
+        pool = Pool()
+        PaymentMean = pool.get('account.invoice.payment.mean')
+        means = []
+        for payment_mean in payment_means:
+            if instrument := cls._parse_2_payment_mean(payment_mean, payees):
+                means.append(PaymentMean(instrument=instrument))
+        return means
+
+    @classmethod
+    def _parse_2_payment_mean(cls, payment_mean, payees):
+        pass
+
+    @classmethod
     def _parse_2_payment_term_date(cls, payment_terms):
         dates = []
         for payment_term in payment_terms:
@@ -1265,6 +1293,80 @@
                     tax_total=lang.format_number(tax_total)))
 
 
+class Invoice_Bank(metaclass=PoolMeta):
+    __name__ = 'edocument.ubl.invoice'
+
+    @classmethod
+    def _parse_2_payment_mean(cls, payment_mean, payees):
+        pool = Pool()
+        Account = pool.get('bank.account')
+        instrument = super()._parse_2_payment_mean(payment_mean, payees)
+        if (financial_account := payment_mean.find(
+                    './{*}PayeeFinancialAccount')) is not None:
+            identifier = financial_account.findtext('./{*}ID')
+            try:
+                account, = Account.search([
+                        ('numbers', 'where', ['OR',
+                                ('number', '=', identifier),
+                                ('number_compact', '=', identifier),
+                                ]),
+                        ('owners.id', 'in', payees),
+                        ], limit=1)
+            except ValueError:
+                party = payees[-1]
+                if party.id in Transaction().create_records['party.party']:
+                    account = cls._create_bank_account(
+                        financial_account, party)
+                    account.save()
+                else:
+                    raise InvoiceError(gettext(
+                            'edocument_ubl.msg_account_not_found',
+                            parties=','.join([p.rec_name for p in payees]),
+                            account=etree.tostring(
+                                financial_account,
+                                pretty_print=True).decode()))
+            instrument = account
+        return instrument
+
+    @classmethod
+    def _create_bank_account(cls, financial_account, party):
+        pool = Pool()
+        Bank = pool.get('bank')
+        Account = pool.get('bank.account')
+        Number = pool.get('bank.account.number')
+        account = Account(owners=[party])
+        if identifier := financial_account.findtext('./{*}ID'):
+            number = Number(number=identifier)
+            if iban.is_valid(identifier):
+                number.type = 'iban'
+            else:
+                number.type = 'other'
+            account.numbers = [number]
+        if identifier := financial_account.findtext(
+                './{*}FinancialInstitutionBranch/{*}ID'):
+            if bic.is_valid(identifier):
+                account.bank = Bank.from_bic(identifier)
+        return account
+
+
+class Invoice_Payment(metaclass=PoolMeta):
+    __name__ = 'edocument.ubl.invoice'
+
+    @classmethod
+    def _parse_invoice_2(cls, root):
+        invoice, attachments = super()._parse_invoice_2(root)
+        if root.find('./{*}PaymentMeans/{*}PaymentMandate') is not None:
+            invoice.payment_direct_debit = True
+        return invoice, attachments
+
+    @classmethod
+    def _parse_credit_note_2(cls, root):
+        invoice, attachments = super()._parse_invoice_2(root)
+        if root.find('./{*}PaymentMeans/{*}PaymentMandate') is not None:
+            invoice.payment_direct_debit = True
+        return invoice, attachments
+
+
 class Invoice_Purchase(metaclass=PoolMeta):
     __name__ = 'edocument.ubl.invoice'
 
diff -r dc28a27b9e81 -r dccdca1b6902 modules/edocument_ubl/setup.py
--- a/modules/edocument_ubl/setup.py    Mon Feb 16 13:18:22 2026 +0100
+++ b/modules/edocument_ubl/setup.py    Mon Feb 16 10:49:41 2026 +0100
@@ -44,7 +44,7 @@
     download_url = 'http://downloads.tryton.org/%s.%s/' % (
         major_version, minor_version)
 
-requires = ['Genshi', 'lxml']
+requires = ['Genshi', 'lxml', '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))
@@ -54,6 +54,7 @@
     get_require_version('proteus'),
     get_require_version('trytond_account_cash_rounding'),
     get_require_version('trytond_account_invoice'),
+    get_require_version('trytond_bank'),
     get_require_version('trytond_document_incoming_invoice'),
     get_require_version('trytond_purchase'),
     get_require_version('trytond_sale'),
diff -r dc28a27b9e81 -r dccdca1b6902 
modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst
--- a/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst      Mon Feb 
16 13:18:22 2026 +0100
+++ b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst      Mon Feb 
16 10:49:41 2026 +0100
@@ -18,7 +18,7 @@
 
 Activate modules::
 
-    >>> modules = ['edocument_ubl', 'account_invoice', 'purchase']
+    >>> modules = ['edocument_ubl', 'account_invoice', 'bank', 'purchase']
     >>> if cash_rounding:
     ...     modules.append('account_cash_rounding')
     >>> config = activate_modules(modules, create_company, create_chart)
@@ -82,6 +82,12 @@
     >>> len(invoice.lines)
     7
 
+    >>> payment_mean, = invoice.payment_means
+    >>> account_number, = payment_mean.instrument.numbers
+    >>> account_number.number
+    'DK1212341234123412'
+    >>> assertEqual(payment_mean.instrument.owners, invoice.alternative_payees)
+
     >>> attachments = Attachment.find([])
     >>> len(attachments)
     3
diff -r dc28a27b9e81 -r dccdca1b6902 modules/edocument_ubl/tryton.cfg
--- a/modules/edocument_ubl/tryton.cfg  Mon Feb 16 13:18:22 2026 +0100
+++ b/modules/edocument_ubl/tryton.cfg  Mon Feb 16 10:49:41 2026 +0100
@@ -7,6 +7,8 @@
 extras_depend:
     account_cash_rounding
     account_invoice
+    account_payment
+    bank
     document_incoming_invoice
     purchase
     sale
@@ -24,6 +26,14 @@
     edocument.Invoice
     account.InvoiceEdocumentStart
 
+[register account_invoice bank]
+model:
+    edocument.Invoice_Bank
+
+[register account_invoice account_payment]
+model:
+    edocument.Invoice_Payment
+
 [register account_invoice purchase]
 model:
     edocument.Invoice_Purchase

Reply via email to