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
