details:   https://code.tryton.org/tryton/commit/237ad99f3031
branch:    default
user:      Cédric Krier <[email protected]>
date:      Thu Nov 20 13:08:49 2025 +0100
description:
        Add parser for UBL invoice and credit note
diffstat:

 modules/edocument_ubl/edocument.py                                   |  890 
+++++++++-
 modules/edocument_ubl/exceptions.py                                  |    8 +
 modules/edocument_ubl/message.xml                                    |   33 +
 modules/edocument_ubl/setup.py                                       |    8 +-
 modules/edocument_ubl/tests/UBL-CreditNote-2.1-Example.xml           |  409 
++++
 modules/edocument_ubl/tests/UBL-Invoice-2.1-Example-Trivial.xml      |   39 +
 modules/edocument_ubl/tests/UBL-Invoice-2.1-Example.xml              |  470 
+++++
 modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.json    |    4 +
 modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst     |   90 +
 modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.json        |    4 +
 modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst         |   90 +
 modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst |   56 +
 modules/edocument_ubl/tests/test_module.py                           |    9 +-
 modules/edocument_ubl/tests/test_scenario.py                         |    8 +
 modules/edocument_ubl/tryton.cfg                                     |    7 +
 15 files changed, 2112 insertions(+), 13 deletions(-)

diffs (2264 lines):

diff -r 99a8099fd8fd -r 237ad99f3031 modules/edocument_ubl/edocument.py
--- a/modules/edocument_ubl/edocument.py        Wed Oct 29 15:55:04 2025 +0100
+++ b/modules/edocument_ubl/edocument.py        Thu Nov 20 13:08:49 2025 +0100
@@ -1,22 +1,35 @@
 # 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 bisect
+import datetime as dt
 import mimetypes
 import os
-from base64 import b64encode
-from itertools import groupby
+from base64 import b64decode, b64encode
+from decimal import Decimal
+from io import BytesIO
+from itertools import chain, groupby
+from operator import itemgetter
 
 import genshi
 import genshi.template
 # XXX fix: https://genshi.edgewall.org/ticket/582
 from genshi.template.astutil import ASTCodeGenerator, ASTTransformer
+from lxml import etree
 
+from trytond.i18n import gettext
 from trytond.model import Model
-from trytond.pool import Pool
+from trytond.modules.product import round_price
+from trytond.pool import Pool, PoolMeta
 from trytond.rpc import RPC
 from trytond.tools import cached_property, slugify
 from trytond.transaction import Transaction
 
+from .exceptions import InvoiceError
+from .party import ISO6523_TYPES
+
+ISO6523 = {v: k for k, v in ISO6523_TYPES.items()}
+
 if not hasattr(ASTCodeGenerator, 'visit_NameConstant'):
     def visit_NameConstant(self, node):
         if node.value is None:
@@ -53,6 +66,7 @@
         super().__setup__()
         cls.__rpc__.update({
                 'render': RPC(instantiate=0),
+                'parse': RPC(readonly=False, result=int),
                 })
 
     def __init__(self, invoice):
@@ -168,3 +182,873 @@
     @cached_property
     def lines(self):
         return [l for l in self.invoice.lines if l.type == 'line']
+
+    @classmethod
+    def parse(cls, document):
+        pool = Pool()
+        Attachment = pool.get('ir.attachment')
+
+        tree = etree.parse(BytesIO(document))
+        root = tree.getroot()
+        namespace = root.nsmap.get(None)
+        invoice, attachments = cls.parser(namespace)(root)
+        invoice.save()
+        invoice.update_taxes()
+        cls.checker(namespace)(root, invoice)
+        attachments = list(attachments)
+        for attachment in attachments:
+            attachment.resource = invoice
+        Attachment.save(attachments)
+        return invoice
+
+    @classmethod
+    def parser(cls, namespace):
+        return {
+            'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2': (
+                cls._parse_invoice_2),
+            'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2': (
+                cls._parse_credit_note_2),
+            }.get(namespace)
+
+    @classmethod
+    def checker(cls, namespace):
+        return {
+            'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2': (
+                cls._check_invoice_2),
+            'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2': (
+                cls._check_credit_note_2),
+            }.get(namespace)
+
+    @classmethod
+    def _parse_invoice_2(cls, root):
+        pool = Pool()
+        Invoice = pool.get('account.invoice')
+        Currency = pool.get('currency.currency')
+
+        type_code = root.findtext('./{*}InvoiceTypeCode')
+        if type_code and type_code != '380':
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_invoice_type_code_unsupported',
+                    type_code=type_code))
+
+        invoice = Invoice(type='in')
+        invoice.reference = root.findtext('./{*}ID')
+        invoice.invoice_date = dt.date.fromisoformat(
+            root.findtext('./{*}IssueDate'))
+        invoice.party = cls._parse_2_supplier(
+            root.find('./{*}AccountingSupplierParty'), create=True)
+        invoice.set_journal()
+        invoice.on_change_party()
+        invoice.invoice_address = cls._parse_2_address(
+            root.find(
+                './{*}AccountingSupplierParty/{*}Party/{*}PostalAddress'),
+            party=invoice.party)
+        invoice.party_tax_identifier = cls._parse_2_tax_identifier(
+            root.findall(
+                './{*}AccountingSupplierParty/{*}Party/{*}PartyTaxScheme'),
+            party=invoice.party, create=True)
+        if (seller := root.find('./{*}SellerSupplierParty')) is not None:
+            supplier = cls._parse_2_supplier(seller)
+        else:
+            supplier = invoice.party
+        if (customer_party := root.find('./{*}AccountingCustomerParty')
+                ) is not None:
+            invoice.company = cls._parse_2_company(customer_party)
+        else:
+            invoice.company = Invoice.default_company()
+        if not invoice.company:
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_company_not_found',
+                    company=etree.tostring(
+                        customer_party, pretty_print=True).decode()
+                    if customer_party else ''))
+
+        if (payee_party := root.find('./{*}PayeeParty')) is not None:
+            party = cls._parse_2_party(payee_party)
+            if not party:
+                party = cls._create_2_party(payee_party)
+                party.save()
+            invoice.alternative_payees = [party]
+
+        currency_code = root.findtext('./{*}DocumentCurrencyCode')
+        if not currency_code:
+            payable_amount = root.find(
+                './{*}LegalMonetaryTotal/{*}PayableAmount')
+            currency_code = payable_amount.get('currencyID')
+        if currency_code is not None:
+            try:
+                invoice.currency, = Currency.search([
+                        ('code', '=', currency_code),
+                        ], limit=1)
+            except ValueError:
+                raise InvoiceError(gettext(
+                        'edocument_ubl.msg_currency_not_found',
+                        code=currency_code))
+
+        invoice.payment_term_date = cls._parse_2_payment_term_date(
+            root.findall('./{*}PaymentTerms'))
+        invoice.lines = [
+            cls._parse_invoice_2_line(
+                line, company=invoice.company, currency=invoice.currency,
+                supplier=supplier)
+            for line in root.iterfind('./{*}InvoiceLine')]
+        invoice.taxes = [
+            cls._parse_2_tax(
+                tax, company=invoice.company)
+            for tax in root.iterfind('./{*}TaxTotal/{*}TaxSubtotal')]
+
+        if (hasattr(Invoice, 'cash_rounding')
+                and (root.find(
+                    './{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
+                    is not None)):
+            invoice.cash_rounding = True
+        return invoice, cls._parse_2_attachments(root)
+
+    @classmethod
+    def _parse_invoice_2_line(
+            cls, invoice_line, company, currency, supplier=None):
+        pool = Pool()
+        Line = pool.get('account.invoice.line')
+        UoM = pool.get('product.uom')
+        Tax = pool.get('account.tax')
+        AccountConfiguration = pool.get('account.configuration')
+
+        account_configuration = AccountConfiguration(1)
+
+        line = Line(type='line', company=company, currency=currency)
+        if (invoiced_quantity := invoice_line.find('./{*}InvoicedQuantity')
+                ) is not None:
+            line.quantity = float(invoiced_quantity.text)
+            if (unit_code := invoiced_quantity.get('unitCode')) is not None:
+                try:
+                    line.unit, = UoM.search([
+                            ('unece_code', '=', unit_code),
+                            ], limit=1)
+                except ValueError:
+                    raise InvoiceError(gettext(
+                            'edocument_ubl.msg_unit_not_found',
+                            code=unit_code))
+        else:
+            line.quantity = 1
+            line.unit = None
+
+        line.product = cls._parse_2_item(
+            invoice_line.find('./{*}Item'), supplier=supplier)
+        if line.product:
+            line.on_change_product()
+
+        line.description = '\n'.join(e.text for e in chain(
+                invoice_line.iterfind('./{*}Item/{*}Name'),
+                invoice_line.iterfind('./{*}Item/{*}BrandName'),
+                invoice_line.iterfind('./{*}Item/{*}ModelName'),
+                invoice_line.iterfind('./{*}Item/{*}Description'),
+                invoice_line.iterfind(
+                    './{*}Item/{*}AdditionalInformation'),
+                invoice_line.iterfind('./{*}Item/{*}WarrantyInformation'),
+                ) if e is not None and e.text)
+
+        if not line.product:
+            if line.description:
+                similar_lines = Line.search([
+                        ('description', 'ilike', line.description),
+                        ('invoice.company', '=', company),
+                        ('invoice.type', '=', 'in'),
+                        ('invoice.state', 'in',
+                            ['validated', 'posted', 'paid']),
+                        ],
+                    order=[('invoice.invoice_date', 'DESC')],
+                    limit=1)
+            else:
+                similar_lines = []
+            if similar_lines:
+                similar_line, = similar_lines
+                line.account = similar_line.account
+                line.product = similar_line.product
+            else:
+                line.account = account_configuration.get_multivalue(
+                    'default_category_account_expense',
+                    company=company.id)
+
+        for line_reference in invoice_line.iterfind('./{*}OrderLineReference'):
+            line.origin = cls._parse_2_line_reference(
+                line_reference, line, company, supplier=supplier)
+            if line.origin:
+                break
+
+        if (price_amount := invoice_line.findtext('./{*}Price/{*}PriceAmount')
+                ) is not None:
+            line.unit_price = round_price(Decimal(price_amount))
+        else:
+            line.unit_price = round_price(
+                Decimal(invoice_line.findtext('./{*}LineExtensionAmount'))
+                / Decimal(str(line.quantity)))
+
+        if invoice_line.find('./{*}Item/{*}ClassifiedTaxCategory') is not None:
+            tax_categories = invoice_line.iterfind(
+                './{*}Item/{*}ClassifiedTaxCategory')
+        else:
+            tax_categories = invoice_line.iterfind(
+                './{*}TaxTotal/{*}TaxSubtotal/{*}TaxCategory')
+        taxes = []
+        for tax_category in tax_categories:
+            domain = cls._parse_2_tax_category(tax_category)
+            domain.extend([
+                    ['OR',
+                        ('group', '=', None),
+                        ('group.kind', 'in', ['purchase', 'both']),
+                        ],
+                    ('company', '=', company.id),
+                    ])
+            try:
+                tax, = Tax.search(domain, limit=1)
+            except ValueError:
+                raise InvoiceError(gettext(
+                        'edocument_ubl.msg_tax_not_found',
+                        tax_category=etree.tostring(
+                            tax_category, pretty_print=True).decode()))
+            taxes.append(tax)
+        line.taxes = taxes
+        return line
+
+    @classmethod
+    def _parse_credit_note_2(cls, root):
+        pool = Pool()
+        Invoice = pool.get('account.invoice')
+        Currency = pool.get('currency.currency')
+
+        type_code = root.findtext('./{*}CreditNoteTypeCode')
+        if type_code and type_code != '381':
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_credit_note_type_code_unsupported',
+                    type_code=type_code))
+
+        invoice = Invoice(type='in')
+        invoice.reference = root.findtext('./{*}ID')
+        invoice.invoice_date = dt.date.fromisoformat(
+            root.findtext('./{*}IssueDate'))
+        invoice.party = cls._parse_2_supplier(
+            root.find('./{*}AccountingSupplierParty'), create=True)
+        invoice.set_journal()
+        invoice.on_change_party()
+        if (seller := root.find('./{*}SellerSupplierParty')) is not None:
+            supplier = cls._parse_2_supplier(seller)
+        else:
+            supplier = invoice.party
+        if (customer_party := root.find('./{*}AccountingCustomerParty')
+                ) is not None:
+            invoice.company = cls._parse_2_company(customer_party)
+        else:
+            invoice.company = Invoice.default_company()
+        if (payee_party := root.find('./{*}PayeeParty')) is not None:
+            party = cls._parse_2_party(payee_party)
+            if not party:
+                party = cls._create_2_party(payee_party)
+                party.save()
+            invoice.alternative_payees = [party]
+        if (currency_code := root.findtext('./{*}DocumentCurrencyCode')
+                ) is not None:
+            try:
+                invoice.currency, = Currency.search([
+                        ('code', '=', currency_code),
+                        ], limit=1)
+            except ValueError:
+                raise InvoiceError(gettext(
+                        'edocument_ubl.msg_currency_not_found',
+                        code=currency_code))
+        invoice.payment_term_date = cls._parse_2_payment_term_date(
+            root.findall('./{*}PaymentTerms'))
+        invoice.lines = [
+            cls._parse_credit_note_2_line(
+                line, company=invoice.company, currency=invoice.currency,
+                supplier=supplier)
+            for line in root.iterfind('./{*}CreditNoteLine')]
+        invoice.taxes = [
+            cls._parse_2_tax(
+                tax, company=invoice.company)
+            for tax in root.iterfind('./{*}TaxTotal/{*}TaxSubtotal')]
+
+        if (hasattr(Invoice, 'cash_rounding')
+                and root.find(
+                    './{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
+                is not None):
+            invoice.cash_rounding = True
+        return invoice, cls._parse_2_attachments(root)
+
+    @classmethod
+    def _parse_credit_note_2_line(
+            cls, credit_note_line, company, currency, supplier=None):
+        pool = Pool()
+        Line = pool.get('account.invoice.line')
+        UoM = pool.get('product.uom')
+        Tax = pool.get('account.tax')
+        AccountConfiguration = pool.get('account.configuration')
+
+        account_configuration = AccountConfiguration(1)
+
+        line = Line(type='line', company=company, currency=currency)
+        if (credited_quantity := credit_note_line.find('./{*}CreditedQuantity')
+                ) is not None:
+            line.quantity = -float(credited_quantity.text)
+            if (unit_code := credited_quantity.get('unitCode')) is not None:
+                try:
+                    line.unit, = UoM.search([
+                            ('unece_code', '=', unit_code),
+                            ], limit=1)
+                except ValueError:
+                    raise InvoiceError(gettext(
+                            'edocument_ubl.msg_unit_not_found',
+                            code=unit_code))
+        else:
+            line.quantity = -1
+            line.unit = None
+
+        line.product = cls._parse_2_item(
+            credit_note_line.find('./{*}Item'), supplier=supplier)
+        if line.product:
+            line.on_change_product()
+
+        line.description = '\n'.join(e.text for e in chain(
+                credit_note_line.iterfind('./{*}Item/{*}Name'),
+                credit_note_line.iterfind('./{*}Item/{*}BrandName'),
+                credit_note_line.iterfind('./{*}Item/{*}ModelName'),
+                credit_note_line.iterfind('./{*}Item/{*}Description'),
+                credit_note_line.iterfind(
+                    './{*}Item/{*}AdditionalInformation'),
+                credit_note_line.iterfind('./{*}Item/{*}WarrantyInformation'),
+                ) if e is not None and e.text)
+
+        if not line.product:
+            if line.description:
+                similar_lines = Line.search([
+                        ('description', 'ilike', line.description),
+                        ('invoice.company', '=', company),
+                        ('invoice.type', '=', 'in'),
+                        ('invoice.state', 'in',
+                            ['validated', 'posted', 'paid']),
+                        ],
+                    order=[('invoice.invoice_date', 'DESC')],
+                    limit=1)
+            else:
+                similar_lines = []
+            if similar_lines:
+                similar_line, = similar_lines
+                line.account = similar_line.account
+                line.product = similar_line.product
+            else:
+                line.account = account_configuration.get_multivalue(
+                    'default_category_account_expense',
+                    company=company.id)
+
+        for line_reference in credit_note_line.iterfind(
+                './{*}OrderLineReference'):
+            line.origin = cls._parse_2_line_reference(
+                line_reference, line, company, supplier=supplier)
+            if line.origin:
+                break
+
+        if (price_amount := credit_note_line.findtext(
+                './{*}Price/{*}PriceAmount')) is not None:
+            line.unit_price = round_price(Decimal(price_amount))
+        else:
+            line.unit_price = round_price(
+                Decimal(credit_note_line.findtext('./{*}LineExtensionAmount'))
+                / line.quantity)
+
+        if (credit_note_line.find('./{*}Item/{*}ClassifiedTaxCategory')
+                is not None):
+            tax_categories = credit_note_line.iterfind(
+                './{*}Item/{*}ClassifiedTaxCategory')
+        else:
+            tax_categories = credit_note_line.iterfind(
+                './{*}TaxTotal/{*}TaxSubtotal/{*}TaxCategory')
+        taxes = []
+        for tax_category in tax_categories:
+            domain = cls._parse_2_tax_category(tax_category)
+            domain.extend([
+                    ['OR',
+                        ('group', '=', None),
+                        ('group.kind', 'in', ['purchase', 'both']),
+                        ],
+                    ('company', '=', company.id),
+                    ])
+            try:
+                tax, = Tax.search(domain, limit=1)
+            except ValueError:
+                raise InvoiceError(gettext(
+                        'edocument_ubl.msg_tax_not_found',
+                        tax_category=etree.tostring(
+                            tax_category, pretty_print=True).decode()))
+            taxes.append(tax)
+        line.taxes = taxes
+        return line
+
+    @classmethod
+    def _parse_2_supplier(cls, supplier_party, create=False):
+        pool = Pool()
+        Party = pool.get('party.party')
+        for account_id in filter(None, chain(
+                    [supplier_party.find('./{*}CustomerAssignedAccountID')],
+                    supplier_party.iterfind('./{*}AdditionalAccountID'))):
+            if account_id.text:
+                try:
+                    party, = Party.search([
+                            ('code', '=', account_id.text),
+                            ])
+                except ValueError:
+                    pass
+                else:
+                    return party
+        party_el = supplier_party.find('./{*}Party')
+        party = cls._parse_2_party(party_el)
+        if not party and create:
+            party = cls._create_2_party(party_el)
+            party.save()
+        return party
+
+    @classmethod
+    def _parse_2_party(cls, party_el):
+        pool = Pool()
+        Party = pool.get('party.party')
+
+        for identifier in party_el.iterfind('./{*}PartyIdentification/{*}ID'):
+            if identifier.text:
+                parties = Party.search([
+                        ('identifiers.code', '=', identifier.text),
+                        ])
+                if len(parties) == 1:
+                    party, = parties
+                    return party
+
+    @classmethod
+    def _create_2_party(cls, party_el):
+        pool = Pool()
+        Party = pool.get('party.party')
+        party = Party()
+        party.name = party_el.findtext('./{*}PartyName/{*}Name')
+        identifiers = []
+        for identifier in party_el.iterfind('./{*}PartyIdentification/{*}ID'):
+            if identifier.text:
+                identifiers.append(cls._create_2_party_identifier(
+                        identifier))
+        party.identifiers = identifiers
+        if (address := party_el.find('./{*}PostalAddress')
+                ) is not None:
+            party.addresses = [cls._create_2_address(address)]
+        return party
+
+    @classmethod
+    def _create_2_party_identifier(cls, identifier):
+        pool = Pool()
+        Identifier = pool.get('party.identifier')
+        if schemeId := identifier.get('schemeID'):
+            type = ISO6523.get(schemeId)
+        else:
+            type = None
+        return Identifier(type=type, code=identifier.text)
+
+    @classmethod
+    def _parse_2_address(cls, address_el, party):
+        pool = Pool()
+        Address = pool.get('party.address')
+
+        address = cls._create_2_address(address_el)
+
+        domain = [('party', '=', party)]
+        for fname in Address._fields:
+            if value := getattr(address, fname, None):
+                domain.append((fname, '=', value))
+        try:
+            address, = Address.search(domain, limit=1)
+        except ValueError:
+            address.party = party
+            address.save()
+        return address
+
+    @classmethod
+    def _create_2_address(cls, address_el):
+        pool = Pool()
+        Address = pool.get('party.address')
+        Country = pool.get('country.country')
+
+        address = Address()
+
+        if address_el is None:
+            return address
+
+        address.post_box = address_el.findtext('./{*}Postbox')
+        address.floor_number = address_el.findtext('./{*}Floor')
+        address.room_number = address_el.findtext('./{*}Room')
+        address.street_name = address_el.findtext('./{*}StreetName')
+        address.building_name = address_el.findtext('./{*}BuildingName')
+        address.building_number = address_el.findtext('./{*}BuildingNumber')
+        address.city = address_el.findtext('./{*}CityName')
+        address.postal_code = address_el.findtext('./{*}PostalZone')
+        if (country_code := address_el.findtext(
+                    './{*}Country/{*}IdentificationCode[@listId="ISO3166-1"]')
+                ) is not None:
+            try:
+                country, = Country.search([
+                        ('code', '=', country_code),
+                        ], limit=1)
+            except ValueError:
+                pass
+            else:
+                address.country = country
+        address.street_unstructured = '\n'.join(
+            (line.text
+                for line in address_el.iterfind('./{*}AddressLine/{*}Line')))
+        return address
+
+    @classmethod
+    def _parse_2_tax_identifier(cls, party_tax_schemes, party, create=False):
+        pool = Pool()
+        Identifier = pool.get('party.identifier')
+
+        tax_identifier_types = party.tax_identifier_types()
+
+        for party_tax_scheme in party_tax_schemes:
+            company_id = party_tax_scheme.find('./{*}CompanyID')
+            if company_id is not None:
+                scheme_id = company_id.get('schemeID')
+                value = company_id.text
+
+                for identifier in party.identifiers:
+                    if (identifier.type in tax_identifier_types
+                            and identifier.iso_6523 == scheme_id
+                            and identifier.code == value):
+                        return identifier
+                else:
+                    if create and scheme_id in ISO6523:
+                        identifier = Identifier(
+                            party=party,
+                            type=ISO6523[scheme_id],
+                            code=value)
+                        identifier.save()
+                        return identifier
+
+    @classmethod
+    def _parse_2_company(cls, customer_party):
+        pool = Pool()
+        Company = pool.get('company.company')
+        try:
+            CustomerCode = pool.get('party.party.customer_code')
+        except KeyError:
+            CustomerCode = None
+
+        if CustomerCode:
+            for account_id in filter(None, chain(
+                        [customer_party.find('./{*}CustomerAssignedAccountID'),
+                            customer_party.find(
+                                './{*}SupplierAssignedAccountID')],
+                        customer_party.iterfind('./{*}AdditionalAccountID'))):
+                if account_id.text:
+                    try:
+                        customer_code, = CustomerCode.search([
+                                ('customer_code', '=', account_id.text),
+                                ])
+                    except ValueError:
+                        pass
+                    else:
+                        return customer_code.company
+        for identifier in customer_party.iterfind(
+                './{*}Party/{*}PartyIdentification/{*}ID'):
+            if identifier.text:
+                companies = Company.search([
+                        ('party.identifiers.code', '=', identifier.text),
+                        ])
+                if len(companies) == 1:
+                    company, = companies
+                    return company
+
+        for company_id in customer_party.iterfind(
+                './{*}Party/{*}PartyTaxScheme/{*}CompanyID'):
+            companies = Company.search([
+                    ('party.identifiers.code', '=', company_id.text),
+                    ])
+            if len(companies) == 1:
+                company, = companies
+                return company
+
+        for name in chain(
+                customer_party.iterfind(
+                    './{*}Party/{*}PartyTaxScheme/{*}RegistrationName'),
+                customer_party.iterfind('./{*}Party/{*}PartyName/{*}Name'),
+                ):
+            if name is None:
+                continue
+            companies = Company.search([
+                    ('party.name', '=', name.text),
+                    ])
+            if len(companies) == 1:
+                company, = companies
+                return company
+
+    @classmethod
+    def _parse_2_payment_term_date(cls, payment_terms):
+        dates = []
+        for payment_term in payment_terms:
+            if (date := payment_term.findtext('./{*}PaymentDueDate')
+                    ) is not None:
+                dates.append(dt.date.fromisoformat(date))
+        return min(dates, default=None)
+
+    @classmethod
+    def _parse_2_item(cls, item, supplier=None):
+        pool = Pool()
+        Product = pool.get('product.product')
+
+        if (identifier := item.find('./{*}StandardItemIdentification/{*}ID')
+                ) is not None:
+            if identifier.get('schemeID') == 'GTIN':
+                try:
+                    product, = Product.search([
+                            ('identifiers', 'where', [
+                                    ('type', 'in', ['ean', 'isbn', 'ismn']),
+                                    ('code', '=', identifier.text),
+                                    ]),
+                            ], limit=1)
+                except ValueError:
+                    pass
+                else:
+                    return product
+        if (code := item.findtext('./{*}BuyersItemIdentification/{*}ID')
+                ) is not None:
+            try:
+                product, = Product.search([
+                        ('code', '=', code),
+                        ], limit=1)
+            except ValueError:
+                pass
+            else:
+                return product
+
+    @classmethod
+    def _parse_2_line_reference(
+            cls, line_reference, line, company, supplier=None):
+        return
+
+    @classmethod
+    def _parse_2_tax_category(cls, tax_category):
+        domain = [
+            ('parent', '=', None),
+            ]
+        if (unece_category_code := tax_category.findtext('./{*}ID')
+                ) is not None:
+            domain.append(('unece_category_code', '=', unece_category_code))
+        if (unece_code := tax_category.findtext('./{*}TaxScheme/{*}ID')
+                ) is not None:
+            domain.append(('unece_code', '=', unece_code))
+        percent = tax_category.findtext('./{*}Percent')
+        if percent:
+            domain.append(('type', '=', 'percentage'))
+            domain.append(('rate', '=', Decimal(percent) / 100))
+        return domain
+
+    @classmethod
+    def _parse_2_tax(cls, tax, company):
+        pool = Pool()
+        Tax = pool.get('account.tax')
+        InvoiceTax = pool.get('account.invoice.tax')
+
+        invoice_tax = InvoiceTax(manual=False)
+
+        tax_category = tax.find('./{*}TaxCategory')
+        domain = cls._parse_2_tax_category(tax_category)
+        domain.extend([
+                ['OR',
+                    ('group', '=', None),
+                    ('group.kind', 'in', ['purchase', 'both']),
+                    ],
+                ('company', '=', company.id),
+                ])
+        try:
+            invoice_tax.tax, = Tax.search(domain, limit=1)
+        except ValueError:
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_tax_not_found',
+                    tax_category=etree.tostring(
+                        tax_category, pretty_print=True).decode()))
+
+        invoice_tax.amount = Decimal(tax.findtext('./{*}TaxAmount'))
+        if (taxable_amount := tax.findtext('./{*}TaxableAmount')) is not None:
+            invoice_tax.base = Decimal(taxable_amount)
+        else:
+            # Use tax amount to define the sign of unknown base
+            invoice_tax.base = invoice_tax.amount
+
+        invoice_tax.on_change_tax()
+
+        return invoice_tax
+
+    @classmethod
+    def _parse_2_attachments(cls, root):
+        pool = Pool()
+        Attachment = pool.get('ir.attachment')
+
+        for name in [
+                'DespatchDocumentReference',
+                'ReceiptDocumentReference',
+                'ContractDocumentReference',
+                'AdditionalDocumentReference',
+                'StatementDocumentReference',
+                'OriginatorDocumentReference'
+                ]:
+            for document in root.iterfind(f'./{{*}}{name}'):
+                attachment = Attachment()
+                name = ' '.join(filter(None, [
+                            document.findtext('./{*}DocumentType'),
+                            document.findtext('./{*}ID'),
+                            ]))
+                if (data := document.find(
+                            './{*}Attachment/{*}EmbeddedDocumentBinaryObject')
+                        ) is not None:
+                    mime_code = (
+                        data.get('mimeCode') or 'application/octet-stream')
+                    name += mimetypes.guess_extension(mime_code) or ''
+                    attachment.type = 'data'
+                    data = b64decode(data.text)
+                    attachment.data = data
+                elif data := document.findtext(
+                        './{*}Attachment/{*}EmbeddedDocument'):
+                    name += '.txt'
+                    attachment.type = 'data'
+                    attachment.data = data
+                elif url := document.findtext(
+                        './{*}Attachment/{*}ExternalReference/{*}URI'):
+                    attachment.type = 'link'
+                    Attachment.link = url
+                attachment.name = name
+                yield attachment
+
+    @classmethod
+    def _check_invoice_2(cls, root, invoice):
+        pool = Pool()
+        Lang = pool.get('ir.lang')
+        lang = Lang.get()
+
+        payable_amount = Decimal(
+            root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount'))
+        prepaid_amount = Decimal(
+            root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount')
+            or 0)
+        amount = payable_amount + prepaid_amount
+        if not getattr(invoice, 'cash_rounding', False):
+            payable_rounding_amount = Decimal(
+                root.findtext(
+                    './{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
+                or 0)
+            amount -= payable_rounding_amount
+        if invoice.total_amount != amount:
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_invoice_total_amount_different',
+                    invoice=invoice.rec_name,
+                    total_amount=lang.format_number(invoice.total_amount),
+                    amount=lang.format_number(amount)))
+
+        tax_total = sum(Decimal(amount.text) for amount in root.iterfind(
+                './{*}TaxTotal/{*}TaxAmount'))
+        if invoice.tax_amount != tax_total:
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_invoice_tax_amount_different',
+                    invoice=invoice.rec_name,
+                    tax_amount=lang.format_number(invoice.tax_amount),
+                    tax_total=lang.format_number(tax_total)))
+
+    @classmethod
+    def _check_credit_note_2(cls, root, invoice):
+        pool = Pool()
+        Lang = pool.get('ir.lang')
+        lang = Lang.get()
+
+        payable_amount = Decimal(
+            root.findtext('./{*}LegalMonetaryTotal/{*}PayableAmount'))
+        prepaid_amount = Decimal(
+            root.findtext('./{*}LegalMonetaryTotal/{*}PrepaidAmount')
+            or 0)
+        amount = payable_amount + prepaid_amount
+        if not getattr(invoice, 'cash_rounding', False):
+            payable_rounding_amount = Decimal(
+                root.findtext(
+                    './{*}LegalMonetaryTotal/{*}PayableRoundingAmount')
+                or 0)
+            amount -= payable_rounding_amount
+        if -invoice.total_amount != amount:
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_invoice_total_amount_different',
+                    invoice=invoice.rec_name,
+                    total_amount=lang.format_number(-invoice.total_amount),
+                    amount=lang.format_number(amount)))
+
+        tax_total = sum(Decimal(amount.text) for amount in root.iterfind(
+                './{*}TaxTotal/{*}TaxAmount'))
+        if -invoice.tax_amount != tax_total:
+            raise InvoiceError(gettext(
+                    'edocument_ubl.msg_invoice_tax_amount_different',
+                    invoice=invoice.rec_name,
+                    tax_amount=lang.format_number(-invoice.tax_amount),
+                    tax_total=lang.format_number(tax_total)))
+
+
+class Invoice_Purchase(metaclass=PoolMeta):
+    __name__ = 'edocument.ubl.invoice'
+
+    @classmethod
+    def _parse_2_item(cls, item, supplier=None):
+        pool = Pool()
+        Product = pool.get('product.product')
+
+        product = super()._parse_2_item(item, supplier=supplier)
+
+        if (not product
+                and supplier
+                and (code := item.findtext(
+                        './{*}SellersItemIdentification/{*}ID'))):
+            try:
+                product, = Product.search([
+                        ('product_suppliers', 'where', [
+                                ('party', '=', supplier.id),
+                                ('code', '=', code),
+                                ]),
+                        ], limit=1)
+            except ValueError:
+                pass
+        return product
+
+    @classmethod
+    def _parse_2_line_reference(
+            cls, line_reference, line, company, supplier=None):
+        pool = Pool()
+        PurchaseLine = pool.get('purchase.line')
+        UoM = pool.get('product.uom')
+
+        origin = super()._parse_2_item_reference(
+            line_reference, line, supplier=supplier)
+        if origin:
+            return origin
+        if not line or not line.product or not line.unit:
+            return
+
+        if numbers := list(filter(None, [
+                        line_reference.findtext('./{*}OrderReference/{*}ID'),
+                        line_reference.findtext(
+                            './{*}OrderReference/{*}SalesOrderID'),
+                        ])):
+            purchase_lines = PurchaseLine.search([
+                    ('purchase.company', '=', company),
+                    ('purchase.rec_name', 'in', numbers),
+                    ('type', '=', 'line'),
+                    ('product', '=', line.product),
+                    ])
+            if purchase_lines:
+                quantities = []
+                for purchase_line in purchase_lines:
+                    quantity = UoM.compute_qty(
+                        purchase_line.unit, purchase_line.quantity, line.unit)
+                    quantities.append((quantity, purchase_line))
+                key = itemgetter(0)
+                quantities.sort(key=key)
+                index = bisect.bisect_left(quantities, line.quantity, key=key)
+                if index >= len(quantities):
+                    index = -1
+                origin = quantities[index][1]
+        return origin
diff -r 99a8099fd8fd -r 237ad99f3031 modules/edocument_ubl/exceptions.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/exceptions.py       Thu Nov 20 13:08:49 2025 +0100
@@ -0,0 +1,8 @@
+# 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 trytond.exceptions import UserError
+
+
+class InvoiceError(UserError):
+    pass
diff -r 99a8099fd8fd -r 237ad99f3031 modules/edocument_ubl/message.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/message.xml Thu Nov 20 13:08:49 2025 +0100
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tryton>
+    <data grouped="1">
+        <record model="ir.message" id="msg_invoice_type_code_unsupported">
+            <field name="text">Unsupported invoice type code 
"%(type_code)s".</field>
+        </record>
+        <record model="ir.message" id="msg_credit_note_type_code_unsupported">
+            <field name="text">Unsupported credit note type code 
"%(type_code)s".</field>
+        </record>
+        <record model="ir.message" id="msg_company_not_found">
+            <field name="text">Could not find the company for:
+%(company)s</field>
+        </record>
+        <record model="ir.message" id="msg_currency_not_found">
+            <field name="text">Could not find a currency with code 
"%(code)s".</field>
+        </record>
+        <record model="ir.message" id="msg_unit_not_found">
+            <field name="text">Could not find a unit with UNECE code 
"%(code)s".</field>
+        </record>
+        <record model="ir.message" id="msg_tax_not_found">
+            <field name="text">Could not find tax for:
+%(tax_category)s</field>
+        </record>
+        <record model="ir.message" id="msg_invoice_total_amount_different">
+            <field name="text">The total amount %(total_amount)s of the 
invoice "%(invoice)s" is different from the amount %(amount)s.</field>
+        </record>
+        <record model="ir.message" id="msg_invoice_tax_amount_different">
+            <field name="text">The tax amount %(tax_amount)s of the invoice 
"%(invoice)s" is different from the tax total %(tax_total)s.</field>
+        </record>
+    </data>
+</tryton>
diff -r 99a8099fd8fd -r 237ad99f3031 modules/edocument_ubl/setup.py
--- a/modules/edocument_ubl/setup.py    Wed Oct 29 15:55:04 2025 +0100
+++ b/modules/edocument_ubl/setup.py    Thu Nov 20 13:08:49 2025 +0100
@@ -44,15 +44,17 @@
     download_url = 'http://downloads.tryton.org/%s.%s/' % (
         major_version, minor_version)
 
-requires = ['Genshi']
+requires = ['Genshi', 'lxml']
 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 = [
-    'lxml',
+    get_require_version('proteus'),
+    get_require_version('trytond_account_cash_rounding'),
     get_require_version('trytond_account_invoice'),
+    get_require_version('trytond_purchase'),
     ]
 
 setup(name=name,
@@ -81,7 +83,7 @@
         'trytond.modules.edocument_ubl': (info.get('xml', [])
             + ['tryton.cfg', 'view/*.xml', 'locale/*.po', '*.fodt',
                 'icons/*.svg', 'template/*/*.xml', 'tests/*/*/*.xsq',
-                'tests/*.rst', 'tests/*.json']),
+                'tests/*.rst', 'tests/*.json', 'tests/*.xml']),
         },
     classifiers=[
         'Development Status :: 5 - Production/Stable',
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/UBL-CreditNote-2.1-Example.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/UBL-CreditNote-2.1-Example.xml        Thu Nov 
20 13:08:49 2025 +0100
@@ -0,0 +1,409 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2" 
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
 
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
+       <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+       <cbc:ID>TOSL108</cbc:ID>
+       <cbc:IssueDate>2009-12-15</cbc:IssueDate>
+       <cbc:Note languageID="en">Ordered in our booth at the 
convention.</cbc:Note>
+       <cbc:DocumentCurrencyCode listID="ISO 4217 Alpha" 
listAgencyID="6">EUR</cbc:DocumentCurrencyCode>
+       <cbc:AccountingCost>Project cost code 123</cbc:AccountingCost>
+       <cac:InvoicePeriod>
+               <cbc:StartDate>2009-11-01</cbc:StartDate>
+               <cbc:EndDate>2009-11-30</cbc:EndDate>
+       </cac:InvoicePeriod>
+       <cac:OrderReference>
+               <cbc:ID>123</cbc:ID>
+       </cac:OrderReference>
+       <cac:ContractDocumentReference>
+               <cbc:ID>Contract321</cbc:ID>
+               <cbc:DocumentType>Framework agreement</cbc:DocumentType>
+       </cac:ContractDocumentReference>
+       <cac:AdditionalDocumentReference>
+               <cbc:ID>Doc1</cbc:ID>
+               <cbc:DocumentType>Timesheet</cbc:DocumentType>
+               <cac:Attachment>
+                       <cac:ExternalReference>
+                               
<cbc:URI>http://www.suppliersite.eu/sheet001.html</cbc:URI>
+                       </cac:ExternalReference>
+               </cac:Attachment>
+       </cac:AdditionalDocumentReference>
+       <cac:AdditionalDocumentReference>
+               <cbc:ID>Doc2</cbc:ID>
+               <cbc:DocumentType>Drawing</cbc:DocumentType>
+               <cac:Attachment>
+                       <cbc:EmbeddedDocumentBinaryObject 
mimeCode="application/pdf">UjBsR09EbGhjZ0dTQUxNQUFBUUNBRU1tQ1p0dU1GUXhEUzhi</cbc:EmbeddedDocumentBinaryObject>
+               </cac:Attachment>
+       </cac:AdditionalDocumentReference>
+       <cac:AccountingSupplierParty>
+               <cac:Party>
+                       <cbc:EndpointID schemeID="GLN" 
schemeAgencyID="9">1234567890123</cbc:EndpointID>
+                       <cac:PartyIdentification>
+                               <cbc:ID schemeID="ZZZ">Supp123</cbc:ID>
+                       </cac:PartyIdentification>
+                       <cac:PartyName>
+                               <cbc:Name>Salescompany ltd.</cbc:Name>
+                       </cac:PartyName>
+                       <cac:PostalAddress>
+                               <cbc:ID schemeID="GLN" 
schemeAgencyID="9">1231412341324</cbc:ID>
+                               <cbc:Postbox>5467</cbc:Postbox>
+                               <cbc:StreetName>Main street</cbc:StreetName>
+                               <cbc:AdditionalStreetName>Suite 
123</cbc:AdditionalStreetName>
+                               <cbc:BuildingNumber>1</cbc:BuildingNumber>
+                               <cbc:Department>Revenue 
department</cbc:Department>
+                               <cbc:CityName>Big city</cbc:CityName>
+                               <cbc:PostalZone>54321</cbc:PostalZone>
+                               
<cbc:CountrySubentityCode>RegionA</cbc:CountrySubentityCode>
+                               <cac:Country>
+                                       <cbc:IdentificationCode 
listID="ISO3166-1" listAgencyID="6">DK</cbc:IdentificationCode>
+                               </cac:Country>
+                       </cac:PostalAddress>
+                       <cac:PartyTaxScheme>
+                               <cbc:CompanyID schemeID="DKVAT" 
schemeAgencyID="ZZZ">DK12345</cbc:CompanyID>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:PartyTaxScheme>
+                       <cac:PartyLegalEntity>
+                               <cbc:RegistrationName>The Sellercompany 
Incorporated</cbc:RegistrationName>
+                               <cbc:CompanyID schemeID="CVR" 
schemeAgencyID="ZZZ">5402697509</cbc:CompanyID>
+                               <cac:RegistrationAddress>
+                                       <cbc:CityName>Big city</cbc:CityName>
+                                       
<cbc:CountrySubentity>RegionA</cbc:CountrySubentity>
+                                       <cac:Country>
+                                               
<cbc:IdentificationCode>DK</cbc:IdentificationCode>
+                                       </cac:Country>
+                               </cac:RegistrationAddress>
+                       </cac:PartyLegalEntity>
+                       <cac:Contact>
+                               <cbc:Telephone>4621230</cbc:Telephone>
+                               <cbc:Telefax>4621231</cbc:Telefax>
+                               
<cbc:ElectronicMail>[email protected]</cbc:ElectronicMail>
+                       </cac:Contact>
+                       <cac:Person>
+                               <cbc:FirstName>Antonio</cbc:FirstName>
+                               <cbc:FamilyName>M</cbc:FamilyName>
+                               <cbc:MiddleName>Salemacher</cbc:MiddleName>
+                               <cbc:JobTitle>Sales manager</cbc:JobTitle>
+                       </cac:Person>
+               </cac:Party>
+       </cac:AccountingSupplierParty>
+       <cac:AccountingCustomerParty>
+               <cac:Party>
+                       <cbc:EndpointID schemeID="GLN" 
schemeAgencyID="9">1234567987654</cbc:EndpointID>
+                       <cac:PartyIdentification>
+                               <cbc:ID schemeID="ZZZ">345KS5324</cbc:ID>
+                       </cac:PartyIdentification>
+                       <cac:PartyName>
+                               <cbc:Name>Buyercompany ltd</cbc:Name>
+                       </cac:PartyName>
+                       <cac:PostalAddress>
+                               <cbc:ID schemeID="GLN" 
schemeAgencyID="9">1238764941386</cbc:ID>
+                               <cbc:Postbox>123</cbc:Postbox>
+                               <cbc:StreetName>Anystreet</cbc:StreetName>
+                               <cbc:AdditionalStreetName>Back 
door</cbc:AdditionalStreetName>
+                               <cbc:BuildingNumber>8</cbc:BuildingNumber>
+                               <cbc:Department>Accounting 
department</cbc:Department>
+                               <cbc:CityName>Anytown</cbc:CityName>
+                               <cbc:PostalZone>101</cbc:PostalZone>
+                               
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
+                               <cac:Country>
+                                       <cbc:IdentificationCode 
listID="ISO3166-1" listAgencyID="6">BE</cbc:IdentificationCode>
+                               </cac:Country>
+                       </cac:PostalAddress>
+                       <cac:PartyTaxScheme>
+                               <cbc:CompanyID schemeID="BEVAT" 
schemeAgencyID="ZZZ">BE54321</cbc:CompanyID>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:PartyTaxScheme>
+                       <cac:PartyLegalEntity>
+                               <cbc:RegistrationName>The buyercompany 
inc.</cbc:RegistrationName>
+                               <cbc:CompanyID schemeAgencyID="ZZZ" 
schemeID="ZZZ">5645342123</cbc:CompanyID>
+                               <cac:RegistrationAddress>
+                                       <cbc:CityName>Mainplace</cbc:CityName>
+                                       
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
+                                       <cac:Country>
+                                               
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
+                                       </cac:Country>
+                               </cac:RegistrationAddress>
+                       </cac:PartyLegalEntity>
+                       <cac:Contact>
+                               <cbc:Telephone>5121230</cbc:Telephone>
+                               <cbc:Telefax>5121231</cbc:Telefax>
+                               
<cbc:ElectronicMail>[email protected]</cbc:ElectronicMail>
+                       </cac:Contact>
+                       <cac:Person>
+                               <cbc:FirstName>John</cbc:FirstName>
+                               <cbc:FamilyName>X</cbc:FamilyName>
+                               <cbc:MiddleName>Doe</cbc:MiddleName>
+                               <cbc:JobTitle>Purchasing manager</cbc:JobTitle>
+                       </cac:Person>
+               </cac:Party>
+       </cac:AccountingCustomerParty>
+       <cac:PayeeParty>
+               <cac:PartyIdentification>
+                       <cbc:ID schemeID="GLN" 
schemeAgencyID="9">098740918237</cbc:ID>
+               </cac:PartyIdentification>
+               <cac:PartyName>
+                       <cbc:Name>Ebeneser Scrooge Inc.</cbc:Name>
+               </cac:PartyName>
+               <cac:PartyLegalEntity>
+                       <cbc:CompanyID schemeID="UK:CH" 
schemeAgencyID="ZZZ">6411982340</cbc:CompanyID>
+               </cac:PartyLegalEntity>
+       </cac:PayeeParty>
+       <cac:AllowanceCharge>
+               <cbc:ChargeIndicator>true</cbc:ChargeIndicator>
+               <cbc:AllowanceChargeReason>Packing 
cost</cbc:AllowanceChargeReason>
+               <cbc:Amount currencyID="EUR">100</cbc:Amount>
+       </cac:AllowanceCharge>
+       <cac:AllowanceCharge>
+               <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+               <cbc:AllowanceChargeReason>Promotion 
discount</cbc:AllowanceChargeReason>
+               <cbc:Amount currencyID="EUR">100</cbc:Amount>
+       </cac:AllowanceCharge>
+       <cac:TaxTotal>
+               <cbc:TaxAmount currencyID="EUR">292.20</cbc:TaxAmount>
+               <cac:TaxSubtotal>
+                       <cbc:TaxableAmount 
currencyID="EUR">1460.5</cbc:TaxableAmount>
+                       <cbc:TaxAmount currencyID="EUR">292.1</cbc:TaxAmount>
+                       <cac:TaxCategory>
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">S</cbc:ID>
+                               <cbc:Percent>20</cbc:Percent>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:TaxCategory>
+               </cac:TaxSubtotal>
+               <cac:TaxSubtotal>
+                       <cbc:TaxableAmount 
currencyID="EUR">1</cbc:TaxableAmount>
+                       <cbc:TaxAmount currencyID="EUR">0.1</cbc:TaxAmount>
+                       <cac:TaxCategory>
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">AA</cbc:ID>
+                               <cbc:Percent>10</cbc:Percent>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:TaxCategory>
+               </cac:TaxSubtotal>
+               <cac:TaxSubtotal>
+                       <cbc:TaxableAmount 
currencyID="EUR">-25</cbc:TaxableAmount>
+                       <cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
+                       <cac:TaxCategory>
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">E</cbc:ID>
+                               <cbc:Percent>0</cbc:Percent>
+                               <cbc:TaxExemptionReasonCode listID="CWA 15577" 
listAgencyID="ZZZ">AAM</cbc:TaxExemptionReasonCode>
+                               <cbc:TaxExemptionReason>Exempt New Means of 
Transport</cbc:TaxExemptionReason>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:TaxCategory>
+               </cac:TaxSubtotal>
+       </cac:TaxTotal>
+       <cac:LegalMonetaryTotal>
+               <cbc:LineExtensionAmount 
currencyID="EUR">1436.5</cbc:LineExtensionAmount>
+               <cbc:TaxExclusiveAmount 
currencyID="EUR">1436.5</cbc:TaxExclusiveAmount>
+               <cbc:TaxInclusiveAmount 
currencyID="EUR">1729</cbc:TaxInclusiveAmount>
+               <cbc:AllowanceTotalAmount 
currencyID="EUR">100</cbc:AllowanceTotalAmount>
+               <cbc:ChargeTotalAmount 
currencyID="EUR">100</cbc:ChargeTotalAmount>
+               <cbc:PrepaidAmount currencyID="EUR">1000</cbc:PrepaidAmount>
+               <cbc:PayableRoundingAmount 
currencyID="EUR">0.30</cbc:PayableRoundingAmount>
+               <cbc:PayableAmount currencyID="EUR">729</cbc:PayableAmount>
+       </cac:LegalMonetaryTotal>
+       <cac:CreditNoteLine>
+               <cbc:ID>1</cbc:ID>
+               <cbc:Note>Scratch on box</cbc:Note>
+               <cbc:CreditedQuantity unitCode="C62">1</cbc:CreditedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">1273</cbc:LineExtensionAmount>
+               <cbc:AccountingCost>BookingCode001</cbc:AccountingCost>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">254.6</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Description languageID="EN">Processor: Intel Core 
2 Duo SU9400 LV (1.4GHz). RAM:
+                               3MB. Screen 1440x900</cbc:Description>
+                       <cbc:Name>Labtop computer</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB007</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890124</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">12344321</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434568</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">S</cbc:ID>
+                               <cbc:Percent>20</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+                       <cac:AdditionalItemProperty>
+                               <cbc:Name>Color</cbc:Name>
+                               <cbc:Value>black</cbc:Value>
+                       </cac:AdditionalItemProperty>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">1273</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+                       <cac:AllowanceCharge>
+                               <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+                               
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
+                               
<cbc:MultiplierFactorNumeric>0.15</cbc:MultiplierFactorNumeric>
+                               <cbc:Amount currencyID="EUR">225</cbc:Amount>
+                               <cbc:BaseAmount 
currencyID="EUR">1500</cbc:BaseAmount>
+                       </cac:AllowanceCharge>
+               </cac:Price>
+       </cac:CreditNoteLine>
+       <cac:CreditNoteLine>
+               <cbc:ID>2</cbc:ID>
+               <cbc:Note>Cover is slightly damaged.</cbc:Note>
+               <cbc:CreditedQuantity unitCode="C62">-1</cbc:CreditedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">-3.96</cbc:LineExtensionAmount>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">-0.396</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>Returned "Advanced computing" book</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB008</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890125</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">32344324</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434567</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">AA</cbc:ID>
+                               <cbc:Percent>10</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">3.96</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+               </cac:Price>
+       </cac:CreditNoteLine>
+       <cac:CreditNoteLine>
+               <cbc:ID>3</cbc:ID>
+               <cbc:CreditedQuantity unitCode="C62">2</cbc:CreditedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">4.96</cbc:LineExtensionAmount>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">0.496</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>"Computing for dummies" book</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB009</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890126</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">32344324</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434566</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">AA</cbc:ID>
+                               <cbc:Percent>10</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">2.48</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+                       <cac:AllowanceCharge>
+                               <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+                               
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
+                               
<cbc:MultiplierFactorNumeric>0.1</cbc:MultiplierFactorNumeric>
+                               <cbc:Amount currencyID="EUR">0.275</cbc:Amount>
+                               <cbc:BaseAmount 
currencyID="EUR">2.75</cbc:BaseAmount>
+                       </cac:AllowanceCharge>
+               </cac:Price>
+       </cac:CreditNoteLine>
+       <cac:CreditNoteLine>
+               <cbc:ID>4</cbc:ID>
+               <cbc:CreditedQuantity unitCode="C62">-1</cbc:CreditedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">-25</cbc:LineExtensionAmount>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>Returned IBM 5150 desktop</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB010</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890127</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">12344322</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434565</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">E</cbc:ID>
+                               <cbc:Percent>0</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">25</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+               </cac:Price>
+       </cac:CreditNoteLine>
+       <cac:CreditNoteLine>
+               <cbc:ID>5</cbc:ID>
+               <cbc:CreditedQuantity unitCode="C62">250</cbc:CreditedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">187.5</cbc:LineExtensionAmount>
+               <cbc:AccountingCost>BookingCode002</cbc:AccountingCost>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">37.5</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>Network cable</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB011</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890128</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">12344325</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434564</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">S</cbc:ID>
+                               <cbc:Percent>20</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+                       <cac:AdditionalItemProperty>
+                               <cbc:Name>Type</cbc:Name>
+                               <cbc:Value>Cat5</cbc:Value>
+                       </cac:AdditionalItemProperty>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">0.75</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+               </cac:Price>
+       </cac:CreditNoteLine>
+</CreditNote>
\ No newline at end of file
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/UBL-Invoice-2.1-Example-Trivial.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/UBL-Invoice-2.1-Example-Trivial.xml   Thu Nov 
20 13:08:49 2025 +0100
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" 
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
 
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
+
+  <cbc:ID>123</cbc:ID>
+  <cbc:IssueDate>2011-09-22</cbc:IssueDate>
+
+  <cac:InvoicePeriod>
+    <cbc:StartDate>2011-08-01</cbc:StartDate>
+    <cbc:EndDate>2011-08-31</cbc:EndDate>
+  </cac:InvoicePeriod>
+
+  <cac:AccountingSupplierParty>
+    <cac:Party>
+      <cac:PartyName>
+        <cbc:Name>Custom Cotter Pins</cbc:Name>
+      </cac:PartyName>
+    </cac:Party>
+  </cac:AccountingSupplierParty>
+  <cac:AccountingCustomerParty>
+    <cac:Party>
+      <cac:PartyName>
+        <cbc:Name>North American Veeblefetzer</cbc:Name>
+      </cac:PartyName>
+    </cac:Party>
+  </cac:AccountingCustomerParty>
+
+  <cac:LegalMonetaryTotal>
+     <cbc:PayableAmount currencyID="CAD">100.00</cbc:PayableAmount>
+  </cac:LegalMonetaryTotal>
+
+  <cac:InvoiceLine>
+    <cbc:ID>1</cbc:ID>
+    <cbc:LineExtensionAmount currencyID="CAD">100.00</cbc:LineExtensionAmount>
+    <cac:Item>
+       <cbc:Description>Cotter pin, MIL-SPEC</cbc:Description>
+    </cac:Item>
+  </cac:InvoiceLine>
+
+</Invoice>
\ No newline at end of file
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/UBL-Invoice-2.1-Example.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/UBL-Invoice-2.1-Example.xml   Thu Nov 20 
13:08:49 2025 +0100
@@ -0,0 +1,470 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" 
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
 
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
+       <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+       <cbc:ID>TOSL108</cbc:ID>
+       <cbc:IssueDate>2009-12-15</cbc:IssueDate>
+       <cbc:InvoiceTypeCode listID="UN/ECE 1001 Subset" 
listAgencyID="6">380</cbc:InvoiceTypeCode>
+       <cbc:Note languageID="en">Ordered in our booth at the 
convention.</cbc:Note>
+       <cbc:TaxPointDate>2009-11-30</cbc:TaxPointDate>
+       <cbc:DocumentCurrencyCode listID="ISO 4217 Alpha" 
listAgencyID="6">EUR</cbc:DocumentCurrencyCode>
+       <cbc:AccountingCost>Project cost code 123</cbc:AccountingCost>
+       <cac:InvoicePeriod>
+               <cbc:StartDate>2009-11-01</cbc:StartDate>
+               <cbc:EndDate>2009-11-30</cbc:EndDate>
+       </cac:InvoicePeriod>
+       <cac:OrderReference>
+               <cbc:ID>123</cbc:ID>
+       </cac:OrderReference>
+       <cac:ContractDocumentReference>
+               <cbc:ID>Contract321</cbc:ID>
+               <cbc:DocumentType>Framework agreement</cbc:DocumentType>
+       </cac:ContractDocumentReference>
+       <cac:AdditionalDocumentReference>
+               <cbc:ID>Doc1</cbc:ID>
+               <cbc:DocumentType>Timesheet</cbc:DocumentType>
+               <cac:Attachment>
+                       <cac:ExternalReference>
+                               
<cbc:URI>http://www.suppliersite.eu/sheet001.html</cbc:URI>
+                       </cac:ExternalReference>
+               </cac:Attachment>
+       </cac:AdditionalDocumentReference>
+       <cac:AdditionalDocumentReference>
+               <cbc:ID>Doc2</cbc:ID>
+               <cbc:DocumentType>Drawing</cbc:DocumentType>
+               <cac:Attachment>
+                       <cbc:EmbeddedDocumentBinaryObject 
mimeCode="application/pdf">UjBsR09EbGhjZ0dTQUxNQUFBUUNBRU1tQ1p0dU1GUXhEUzhi</cbc:EmbeddedDocumentBinaryObject>
+               </cac:Attachment>
+       </cac:AdditionalDocumentReference>
+       <cac:AccountingSupplierParty>
+               <cac:Party>
+                       <cbc:EndpointID schemeID="GLN" 
schemeAgencyID="9">1234567890123</cbc:EndpointID>
+                       <cac:PartyIdentification>
+                               <cbc:ID schemeID="ZZZ">Supp123</cbc:ID>
+                       </cac:PartyIdentification>
+                       <cac:PartyName>
+                               <cbc:Name>Salescompany ltd.</cbc:Name>
+                       </cac:PartyName>
+                       <cac:PostalAddress>
+                               <cbc:ID schemeID="GLN" 
schemeAgencyID="9">1231412341324</cbc:ID>
+                               <cbc:Postbox>5467</cbc:Postbox>
+                               <cbc:StreetName>Main street</cbc:StreetName>
+                               <cbc:AdditionalStreetName>Suite 
123</cbc:AdditionalStreetName>
+                               <cbc:BuildingNumber>1</cbc:BuildingNumber>
+                               <cbc:Department>Revenue 
department</cbc:Department>
+                               <cbc:CityName>Big city</cbc:CityName>
+                               <cbc:PostalZone>54321</cbc:PostalZone>
+                               
<cbc:CountrySubentityCode>RegionA</cbc:CountrySubentityCode>
+                               <cac:Country>
+                                       <cbc:IdentificationCode 
listID="ISO3166-1" listAgencyID="6">DK</cbc:IdentificationCode>
+                               </cac:Country>
+                       </cac:PostalAddress>
+                       <cac:PartyTaxScheme>
+                               <cbc:CompanyID schemeID="DKVAT" 
schemeAgencyID="ZZZ">DK12345</cbc:CompanyID>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:PartyTaxScheme>
+                       <cac:PartyLegalEntity>
+                               <cbc:RegistrationName>The Sellercompany 
Incorporated</cbc:RegistrationName>
+                               <cbc:CompanyID schemeID="CVR" 
schemeAgencyID="ZZZ">5402697509</cbc:CompanyID>
+                               <cac:RegistrationAddress>
+                                       <cbc:CityName>Big city</cbc:CityName>
+                                       
<cbc:CountrySubentity>RegionA</cbc:CountrySubentity>
+                                       <cac:Country>
+                                               
<cbc:IdentificationCode>DK</cbc:IdentificationCode>
+                                       </cac:Country>
+                               </cac:RegistrationAddress>
+                       </cac:PartyLegalEntity>
+                       <cac:Contact>
+                               <cbc:Telephone>4621230</cbc:Telephone>
+                               <cbc:Telefax>4621231</cbc:Telefax>
+                               
<cbc:ElectronicMail>[email protected]</cbc:ElectronicMail>
+                       </cac:Contact>
+                       <cac:Person>
+                               <cbc:FirstName>Antonio</cbc:FirstName>
+                               <cbc:FamilyName>Salemacher</cbc:FamilyName>
+                               <cbc:MiddleName>M</cbc:MiddleName>
+                               <cbc:JobTitle>Sales manager</cbc:JobTitle>
+                       </cac:Person>
+               </cac:Party>
+       </cac:AccountingSupplierParty>
+       <cac:AccountingCustomerParty>
+               <cac:Party>
+                       <cbc:EndpointID schemeID="GLN" 
schemeAgencyID="9">1234567987654</cbc:EndpointID>
+                       <cac:PartyIdentification>
+                               <cbc:ID schemeID="ZZZ">345KS5324</cbc:ID>
+                       </cac:PartyIdentification>
+                       <cac:PartyName>
+                               <cbc:Name>Buyercompany ltd</cbc:Name>
+                       </cac:PartyName>
+                       <cac:PostalAddress>
+                               <cbc:ID schemeID="GLN" 
schemeAgencyID="9">1238764941386</cbc:ID>
+                               <cbc:Postbox>123</cbc:Postbox>
+                               <cbc:StreetName>Anystreet</cbc:StreetName>
+                               <cbc:AdditionalStreetName>Back 
door</cbc:AdditionalStreetName>
+                               <cbc:BuildingNumber>8</cbc:BuildingNumber>
+                               <cbc:Department>Accounting 
department</cbc:Department>
+                               <cbc:CityName>Anytown</cbc:CityName>
+                               <cbc:PostalZone>101</cbc:PostalZone>
+                               
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
+                               <cac:Country>
+                                       <cbc:IdentificationCode 
listID="ISO3166-1" listAgencyID="6">BE</cbc:IdentificationCode>
+                               </cac:Country>
+                       </cac:PostalAddress>
+                       <cac:PartyTaxScheme>
+                               <cbc:CompanyID schemeID="BEVAT" 
schemeAgencyID="ZZZ">BE54321</cbc:CompanyID>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:PartyTaxScheme>
+                       <cac:PartyLegalEntity>
+                               <cbc:RegistrationName>The buyercompany 
inc.</cbc:RegistrationName>
+                               <cbc:CompanyID schemeAgencyID="ZZZ" 
schemeID="ZZZ">5645342123</cbc:CompanyID>
+                               <cac:RegistrationAddress>
+                                       <cbc:CityName>Mainplace</cbc:CityName>
+                                       
<cbc:CountrySubentity>RegionB</cbc:CountrySubentity>
+                                       <cac:Country>
+                                               
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
+                                       </cac:Country>
+                               </cac:RegistrationAddress>
+                       </cac:PartyLegalEntity>
+                       <cac:Contact>
+                               <cbc:Telephone>5121230</cbc:Telephone>
+                               <cbc:Telefax>5121231</cbc:Telefax>
+                               
<cbc:ElectronicMail>[email protected]</cbc:ElectronicMail>
+                       </cac:Contact>
+                       <cac:Person>
+                               <cbc:FirstName>John</cbc:FirstName>
+                               <cbc:FamilyName>Doe</cbc:FamilyName>
+                               <cbc:MiddleName>X</cbc:MiddleName>
+                               <cbc:JobTitle>Purchasing manager</cbc:JobTitle>
+                       </cac:Person>
+               </cac:Party>
+       </cac:AccountingCustomerParty>
+       <cac:PayeeParty>
+               <cac:PartyIdentification>
+                       <cbc:ID schemeID="GLN" 
schemeAgencyID="9">098740918237</cbc:ID>
+               </cac:PartyIdentification>
+               <cac:PartyName>
+                       <cbc:Name>Ebeneser Scrooge Inc.</cbc:Name>
+               </cac:PartyName>
+               <cac:PartyLegalEntity>
+                       <cbc:CompanyID schemeID="UK:CH" 
schemeAgencyID="ZZZ">6411982340</cbc:CompanyID>
+               </cac:PartyLegalEntity>
+       </cac:PayeeParty>
+       <cac:Delivery>
+               <cbc:ActualDeliveryDate>2009-12-15</cbc:ActualDeliveryDate>
+               <cac:DeliveryLocation>
+                       <cbc:ID schemeID="GLN" 
schemeAgencyID="9">6754238987648</cbc:ID>
+                       <cac:Address>
+                               <cbc:StreetName>Deliverystreet</cbc:StreetName>
+                               <cbc:AdditionalStreetName>Side 
door</cbc:AdditionalStreetName>
+                               <cbc:BuildingNumber>12</cbc:BuildingNumber>
+                               <cbc:CityName>DeliveryCity</cbc:CityName>
+                               <cbc:PostalZone>523427</cbc:PostalZone>
+                               
<cbc:CountrySubentity>RegionC</cbc:CountrySubentity>
+                               <cac:Country>
+                                       
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
+                               </cac:Country>
+                       </cac:Address>
+               </cac:DeliveryLocation>
+       </cac:Delivery>
+       <cac:PaymentMeans>
+               <cbc:PaymentMeansCode listID="UN/ECE 
4461">31</cbc:PaymentMeansCode>
+               <cbc:PaymentDueDate>2009-12-31</cbc:PaymentDueDate>
+               <cbc:PaymentChannelCode>IBAN</cbc:PaymentChannelCode>
+               <cbc:PaymentID>Payref1</cbc:PaymentID>
+               <cac:PayeeFinancialAccount>
+                       <cbc:ID>DK1212341234123412</cbc:ID>
+                       <cac:FinancialInstitutionBranch>
+                               <cac:FinancialInstitution>
+                                       <cbc:ID>DKDKABCD</cbc:ID>
+                               </cac:FinancialInstitution>
+                       </cac:FinancialInstitutionBranch>
+               </cac:PayeeFinancialAccount>
+       </cac:PaymentMeans>
+       <cac:PaymentTerms>
+               <cbc:Note>Penalty percentage 10% from due date</cbc:Note>
+       </cac:PaymentTerms>
+       <cac:AllowanceCharge>
+               <cbc:ChargeIndicator>true</cbc:ChargeIndicator>
+               <cbc:AllowanceChargeReason>Packing 
cost</cbc:AllowanceChargeReason>
+               <cbc:Amount currencyID="EUR">100</cbc:Amount>
+       </cac:AllowanceCharge>
+       <cac:AllowanceCharge>
+               <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+               <cbc:AllowanceChargeReason>Promotion 
discount</cbc:AllowanceChargeReason>
+               <cbc:Amount currencyID="EUR">100</cbc:Amount>
+       </cac:AllowanceCharge>
+       <cac:TaxTotal>
+               <cbc:TaxAmount currencyID="EUR">292.20</cbc:TaxAmount>
+               <cac:TaxSubtotal>
+                       <cbc:TaxableAmount 
currencyID="EUR">1460.5</cbc:TaxableAmount>
+                       <cbc:TaxAmount currencyID="EUR">292.1</cbc:TaxAmount>
+                       <cac:TaxCategory>
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">S</cbc:ID>
+                               <cbc:Percent>20</cbc:Percent>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:TaxCategory>
+               </cac:TaxSubtotal>
+               <cac:TaxSubtotal>
+                       <cbc:TaxableAmount 
currencyID="EUR">1</cbc:TaxableAmount>
+                       <cbc:TaxAmount currencyID="EUR">0.1</cbc:TaxAmount>
+                       <cac:TaxCategory>
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">AA</cbc:ID>
+                               <cbc:Percent>10</cbc:Percent>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:TaxCategory>
+               </cac:TaxSubtotal>
+               <cac:TaxSubtotal>
+                       <cbc:TaxableAmount 
currencyID="EUR">-25</cbc:TaxableAmount>
+                       <cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
+                       <cac:TaxCategory>
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">E</cbc:ID>
+                               <cbc:Percent>0</cbc:Percent>
+                               <cbc:TaxExemptionReasonCode listID="CWA 15577" 
listAgencyID="ZZZ">AAM</cbc:TaxExemptionReasonCode>
+                               <cbc:TaxExemptionReason>Exempt New Means of 
Transport</cbc:TaxExemptionReason>
+                               <cac:TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </cac:TaxScheme>
+                       </cac:TaxCategory>
+               </cac:TaxSubtotal>
+       </cac:TaxTotal>
+       <cac:LegalMonetaryTotal>
+               <cbc:LineExtensionAmount 
currencyID="EUR">1436.5</cbc:LineExtensionAmount>
+               <cbc:TaxExclusiveAmount 
currencyID="EUR">1436.5</cbc:TaxExclusiveAmount>
+               <cbc:TaxInclusiveAmount 
currencyID="EUR">1729</cbc:TaxInclusiveAmount>
+               <cbc:AllowanceTotalAmount 
currencyID="EUR">100</cbc:AllowanceTotalAmount>
+               <cbc:ChargeTotalAmount 
currencyID="EUR">100</cbc:ChargeTotalAmount>
+               <cbc:PrepaidAmount currencyID="EUR">1000</cbc:PrepaidAmount>
+               <cbc:PayableRoundingAmount 
currencyID="EUR">0.30</cbc:PayableRoundingAmount>
+               <cbc:PayableAmount currencyID="EUR">729</cbc:PayableAmount>
+       </cac:LegalMonetaryTotal>
+       <cac:InvoiceLine>
+               <cbc:ID>1</cbc:ID>
+               <cbc:Note>Scratch on box</cbc:Note>
+               <cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">1273</cbc:LineExtensionAmount>
+               <cbc:AccountingCost>BookingCode001</cbc:AccountingCost>
+               <cac:OrderLineReference>
+                       <cbc:LineID>1</cbc:LineID>
+               </cac:OrderLineReference>
+               <cac:AllowanceCharge>
+                       <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+                       
<cbc:AllowanceChargeReason>Damage</cbc:AllowanceChargeReason>
+                       <cbc:Amount currencyID="EUR">12</cbc:Amount>
+               </cac:AllowanceCharge>
+               <cac:AllowanceCharge>
+                       <cbc:ChargeIndicator>true</cbc:ChargeIndicator>
+                       
<cbc:AllowanceChargeReason>Testing</cbc:AllowanceChargeReason>
+                       <cbc:Amount currencyID="EUR">10</cbc:Amount>
+               </cac:AllowanceCharge>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">254.6</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Description languageID="EN">Processor: Intel Core 
2 Duo SU9400 LV (1.4GHz). RAM:
+                               3MB. Screen 1440x900</cbc:Description>
+                       <cbc:Name>Labtop computer</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB007</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890124</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">12344321</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434568</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">S</cbc:ID>
+                               <cbc:Percent>20</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+                       <cac:AdditionalItemProperty>
+                               <cbc:Name>Color</cbc:Name>
+                               <cbc:Value>black</cbc:Value>
+                       </cac:AdditionalItemProperty>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">1273</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+                       <cac:AllowanceCharge>
+                               <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+                               
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
+                               
<cbc:MultiplierFactorNumeric>0.15</cbc:MultiplierFactorNumeric>
+                               <cbc:Amount currencyID="EUR">225</cbc:Amount>
+                               <cbc:BaseAmount 
currencyID="EUR">1500</cbc:BaseAmount>
+                       </cac:AllowanceCharge>
+               </cac:Price>
+       </cac:InvoiceLine>
+       <cac:InvoiceLine>
+               <cbc:ID>2</cbc:ID>
+               <cbc:Note>Cover is slightly damaged.</cbc:Note>
+               <cbc:InvoicedQuantity unitCode="C62">-1</cbc:InvoicedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">-3.96</cbc:LineExtensionAmount>
+               <cac:OrderLineReference>
+                       <cbc:LineID>5</cbc:LineID>
+               </cac:OrderLineReference>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">-0.396</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>Returned "Advanced computing" book</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB008</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890125</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">32344324</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434567</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">AA</cbc:ID>
+                               <cbc:Percent>10</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">3.96</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+               </cac:Price>
+       </cac:InvoiceLine>
+       <cac:InvoiceLine>
+               <cbc:ID>3</cbc:ID>
+               <cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">4.96</cbc:LineExtensionAmount>
+               <cac:OrderLineReference>
+                       <cbc:LineID>3</cbc:LineID>
+               </cac:OrderLineReference>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">0.496</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>"Computing for dummies" book</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB009</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890126</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">32344324</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434566</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">AA</cbc:ID>
+                               <cbc:Percent>10</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">2.48</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+                       <cac:AllowanceCharge>
+                               <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
+                               
<cbc:AllowanceChargeReason>Contract</cbc:AllowanceChargeReason>
+                               
<cbc:MultiplierFactorNumeric>0.1</cbc:MultiplierFactorNumeric>
+                               <cbc:Amount currencyID="EUR">0.275</cbc:Amount>
+                               <cbc:BaseAmount 
currencyID="EUR">2.75</cbc:BaseAmount>
+                       </cac:AllowanceCharge>
+               </cac:Price>
+       </cac:InvoiceLine>
+       <cac:InvoiceLine>
+               <cbc:ID>4</cbc:ID>
+               <cbc:InvoicedQuantity unitCode="C62">-1</cbc:InvoicedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">-25</cbc:LineExtensionAmount>
+               <cac:OrderLineReference>
+                       <cbc:LineID>2</cbc:LineID>
+               </cac:OrderLineReference>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">0</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>Returned IBM 5150 desktop</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB010</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890127</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">12344322</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434565</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">E</cbc:ID>
+                               <cbc:Percent>0</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">25</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+               </cac:Price>
+       </cac:InvoiceLine>
+       <cac:InvoiceLine>
+               <cbc:ID>5</cbc:ID>
+               <cbc:InvoicedQuantity unitCode="C62">250</cbc:InvoicedQuantity>
+               <cbc:LineExtensionAmount 
currencyID="EUR">187.5</cbc:LineExtensionAmount>
+               <cbc:AccountingCost>BookingCode002</cbc:AccountingCost>
+               <cac:OrderLineReference>
+                       <cbc:LineID>4</cbc:LineID>
+               </cac:OrderLineReference>
+               <cac:TaxTotal>
+                       <cbc:TaxAmount currencyID="EUR">37.5</cbc:TaxAmount>
+               </cac:TaxTotal>
+               <cac:Item>
+                       <cbc:Name>Network cable</cbc:Name>
+                       <cac:SellersItemIdentification>
+                               <cbc:ID>JB011</cbc:ID>
+                       </cac:SellersItemIdentification>
+                       <cac:StandardItemIdentification>
+                               <cbc:ID schemeID="GTIN" 
schemeAgencyID="9">1234567890128</cbc:ID>
+                       </cac:StandardItemIdentification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="113" 
listID="UNSPSC">12344325</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <cac:CommodityClassification>
+                               <cbc:ItemClassificationCode listAgencyID="2" 
listID="CPV">65434564</cbc:ItemClassificationCode>
+                       </cac:CommodityClassification>
+                       <ClassifiedTaxCategory 
xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
+                               <cbc:ID schemeID="UN/ECE 5305" 
schemeAgencyID="6">S</cbc:ID>
+                               <cbc:Percent>20</cbc:Percent>
+                               <TaxScheme>
+                                       <cbc:ID schemeID="UN/ECE 5153" 
schemeAgencyID="6">VAT</cbc:ID>
+                               </TaxScheme>
+                       </ClassifiedTaxCategory>
+                       <cac:AdditionalItemProperty>
+                               <cbc:Name>Type</cbc:Name>
+                               <cbc:Value>Cat5</cbc:Value>
+                       </cac:AdditionalItemProperty>
+               </cac:Item>
+               <cac:Price>
+                       <cbc:PriceAmount currencyID="EUR">0.75</cbc:PriceAmount>
+                       <cbc:BaseQuantity unitCode="C62">1</cbc:BaseQuantity>
+               </cac:Price>
+       </cac:InvoiceLine>
+</Invoice>
\ No newline at end of file
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.json
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.json Thu Nov 
20 13:08:49 2025 +0100
@@ -0,0 +1,4 @@
+[
+    {"cash_rounding": true}
+    ,{"cash_rounding": false}
+]
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/scenario_ubl_2_credit_note_parse.rst  Thu Nov 
20 13:08:49 2025 +0100
@@ -0,0 +1,90 @@
+=======================
+UBL 2 Credit Note Parse
+=======================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model
+    >>> from trytond.modules.account.tests.tools import create_chart, 
create_tax
+    >>> from trytond.modules.company.tests.tools import create_company, 
get_company
+    >>> from trytond.modules.currency.tests.tools import get_currency
+    >>> from trytond.tests.tools import activate_modules, assertEqual
+    >>> from trytond.tools import file_open
+
+    >>> cash_rounding = globals().get('cash_rounding', False)
+
+Activate modules::
+
+    >>> modules = ['edocument_ubl', 'account_invoice']
+    >>> if cash_rounding:
+    ...     modules.append('account_cash_rounding')
+    >>> config = activate_modules(modules, create_company, create_chart)
+
+    >>> Attachment = Model.get('ir.attachment')
+    >>> EInvoice = Model.get('edocument.ubl.invoice')
+    >>> Invoice = Model.get('account.invoice')
+
+Setup company::
+
+    >>> company = get_company()
+    >>> company.party.name = 'Buyercompany ltd'
+    >>> company.party.save()
+
+Create currency::
+
+    >>> eur = get_currency('EUR')
+    >>> if cash_rounding:
+    ...     eur.cash_rounding = Decimal('1')
+    >>> eur.save()
+
+Create tax::
+
+    >>> tax20 = create_tax(Decimal('.2'))
+    >>> tax20.unece_category_code = 'S'
+    >>> tax20.unece_code = 'VAT'
+    >>> tax20.save()
+
+    >>> tax10 = create_tax(Decimal('.1'))
+    >>> tax10.unece_category_code = 'AA'
+    >>> tax10.unece_code = 'VAT'
+    >>> tax10.save()
+
+    >>> tax0 = create_tax(Decimal('0'))
+    >>> tax0.unece_category_code = 'E'
+    >>> tax0.unece_code = 'VAT'
+    >>> tax0.save()
+
+Parse the UBL invoice::
+
+    >>> with file_open(
+    ...         'edocument_ubl/tests/UBL-CreditNote-2.1-Example.xml',
+    ...         mode='rb') as fp:
+    ...     invoice_id = EInvoice.parse(fp.read(), config.context)
+
+    >>> invoice = Invoice(invoice_id)
+
+    >>> invoice.reference
+    'TOSL108'
+    >>> assertEqual(invoice.invoice_date, dt.date(2009, 12, 15))
+    >>> invoice.party.name
+    'Salescompany ltd.'
+    >>> invoice.invoice_address.rec_name
+    'Salescompany ltd., Main street 1 5467, 54321, Big city'
+    >>> assertEqual(invoice.company, company)
+    >>> assertEqual(
+    ...     invoice.total_amount,
+    ...     Decimal('-1729.00') if cash_rounding else Decimal('-1728.70'))
+    >>> invoice.tax_amount
+    Decimal('-292.20')
+    >>> len(invoice.lines)
+    5
+
+    >>> attachments = Attachment.find([])
+    >>> len(attachments)
+    3
+    >>> assertEqual({a.resource for a in attachments}, {invoice})
+    >>> sorted((a.name, a.type) for a in attachments)
+    [('Drawing Doc2.pdf', 'data'), ('Framework agreement Contract321', 
'data'), ('Timesheet Doc1', 'link')]
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.json
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.json     Thu Nov 
20 13:08:49 2025 +0100
@@ -0,0 +1,4 @@
+[
+    {"cash_rounding": true}
+    ,{"cash_rounding": false}
+]
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse.rst      Thu Nov 
20 13:08:49 2025 +0100
@@ -0,0 +1,90 @@
+===================
+UBL 2 Invoice Parse
+===================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> from decimal import Decimal
+
+    >>> from proteus import Model
+    >>> from trytond.modules.account.tests.tools import create_chart, 
create_tax
+    >>> from trytond.modules.company.tests.tools import create_company, 
get_company
+    >>> from trytond.modules.currency.tests.tools import get_currency
+    >>> from trytond.tests.tools import activate_modules, assertEqual
+    >>> from trytond.tools import file_open
+
+    >>> cash_rounding = globals().get('cash_rounding', False)
+
+Activate modules::
+
+    >>> modules = ['edocument_ubl', 'account_invoice']
+    >>> if cash_rounding:
+    ...     modules.append('account_cash_rounding')
+    >>> config = activate_modules(modules, create_company, create_chart)
+
+    >>> Attachment = Model.get('ir.attachment')
+    >>> EInvoice = Model.get('edocument.ubl.invoice')
+    >>> Invoice = Model.get('account.invoice')
+
+Setup company::
+
+    >>> company = get_company()
+    >>> company.party.name = 'Buyercompany ltd'
+    >>> company.party.save()
+
+Create currency::
+
+    >>> eur = get_currency('EUR')
+    >>> if cash_rounding:
+    ...     eur.cash_rounding = Decimal('1')
+    >>> eur.save()
+
+Create tax::
+
+    >>> tax20 = create_tax(Decimal('.2'))
+    >>> tax20.unece_category_code = 'S'
+    >>> tax20.unece_code = 'VAT'
+    >>> tax20.save()
+
+    >>> tax10 = create_tax(Decimal('.1'))
+    >>> tax10.unece_category_code = 'AA'
+    >>> tax10.unece_code = 'VAT'
+    >>> tax10.save()
+
+    >>> tax0 = create_tax(Decimal('0'))
+    >>> tax0.unece_category_code = 'E'
+    >>> tax0.unece_code = 'VAT'
+    >>> tax0.save()
+
+Parse the UBL invoice::
+
+    >>> with file_open(
+    ...         'edocument_ubl/tests/UBL-Invoice-2.1-Example.xml',
+    ...         mode='rb') as fp:
+    ...     invoice_id = EInvoice.parse(fp.read(), config.context)
+
+    >>> invoice = Invoice(invoice_id)
+
+    >>> invoice.reference
+    'TOSL108'
+    >>> assertEqual(invoice.invoice_date, dt.date(2009, 12, 15))
+    >>> invoice.party.name
+    'Salescompany ltd.'
+    >>> invoice.invoice_address.rec_name
+    'Salescompany ltd., Main street 1 5467, 54321, Big city'
+    >>> assertEqual(invoice.company, company)
+    >>> assertEqual(
+    ...     invoice.total_amount,
+    ...     Decimal('1729.00') if cash_rounding else Decimal('1728.70'))
+    >>> invoice.tax_amount
+    Decimal('292.20')
+    >>> len(invoice.lines)
+    5
+
+    >>> attachments = Attachment.find([])
+    >>> len(attachments)
+    3
+    >>> assertEqual({a.resource for a in attachments}, {invoice})
+    >>> sorted((a.name, a.type) for a in attachments)
+    [('Drawing Doc2.pdf', 'data'), ('Framework agreement Contract321', 
'data'), ('Timesheet Doc1', 'link')]
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/scenario_ubl_2_invoice_parse_trivial.rst      
Thu Nov 20 13:08:49 2025 +0100
@@ -0,0 +1,56 @@
+===========================
+UBL 2 Invoice Parse Trivial
+===========================
+
+Imports::
+
+    >>> import datetime as dt
+
+    >>> from proteus import Model
+    >>> from trytond.modules.account.tests.tools import create_chart
+    >>> from trytond.modules.company.tests.tools import create_company, 
get_company
+    >>> from trytond.modules.currency.tests.tools import get_currency
+    >>> from trytond.tests.tools import activate_modules, assertEqual
+    >>> from trytond.tools import file_open
+
+Activate modules::
+
+    >>> config = activate_modules(
+    ...     ['edocument_ubl', 'account_invoice'], create_company, create_chart)
+
+    >>> EInvoice = Model.get('edocument.ubl.invoice')
+    >>> Invoice = Model.get('account.invoice')
+
+Setup company::
+
+    >>> company = get_company()
+    >>> company.party.name = 'North American Veeblefetzer'
+    >>> company.party.save()
+
+Create currency::
+
+    >>> eur = get_currency('CAD')
+
+Parse the UBL invoice::
+
+    >>> with file_open(
+    ...         'edocument_ubl/tests/UBL-Invoice-2.1-Example-Trivial.xml',
+    ...         mode='rb') as fp:
+    ...     invoice_id = EInvoice.parse(fp.read(), config.context)
+
+    >>> invoice = Invoice(invoice_id)
+
+    >>> assertEqual(invoice.invoice_date, dt.date(2011, 9, 22))
+    >>> invoice.party.name
+    'Custom Cotter Pins'
+    >>> assertEqual(invoice.company, company)
+    >>> invoice.total_amount
+    Decimal('100.00')
+    >>> line, = invoice.lines
+    >>> line.description
+    'Cotter pin, MIL-SPEC'
+    >>> line.quantity
+    1.0
+    >>> line.unit
+    >>> line.unit_price
+    Decimal('100.0000')
diff -r 99a8099fd8fd -r 237ad99f3031 modules/edocument_ubl/tests/test_module.py
--- a/modules/edocument_ubl/tests/test_module.py        Wed Oct 29 15:55:04 
2025 +0100
+++ b/modules/edocument_ubl/tests/test_module.py        Thu Nov 20 13:08:49 
2025 +0100
@@ -9,8 +9,7 @@
 from lxml import etree
 
 from trytond.pool import Pool
-from trytond.tests.test_tryton import (
-    ModuleTestCase, activate_module, with_transaction)
+from trytond.tests.test_tryton import ModuleTestCase, with_transaction
 
 
 def get_invoice():
@@ -135,11 +134,7 @@
 class EdocumentUblTestCase(ModuleTestCase):
     "Test Edocument Ubl module"
     module = 'edocument_ubl'
-
-    @classmethod
-    def setUpClass(cls):
-        super().setUpClass()
-        activate_module('account_invoice')
+    extras = ['account_invoice', 'purchase']
 
     @with_transaction()
     def test_Invoice_2(self):
diff -r 99a8099fd8fd -r 237ad99f3031 
modules/edocument_ubl/tests/test_scenario.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/edocument_ubl/tests/test_scenario.py      Thu Nov 20 13:08:49 
2025 +0100
@@ -0,0 +1,8 @@
+# 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 trytond.tests.test_tryton import load_doc_tests
+
+
+def load_tests(*args, **kwargs):
+    return load_doc_tests(__name__, __file__, *args, **kwargs)
diff -r 99a8099fd8fd -r 237ad99f3031 modules/edocument_ubl/tryton.cfg
--- a/modules/edocument_ubl/tryton.cfg  Wed Oct 29 15:55:04 2025 +0100
+++ b/modules/edocument_ubl/tryton.cfg  Thu Nov 20 13:08:49 2025 +0100
@@ -5,9 +5,12 @@
     ir
     party
 extras_depend:
+    account_cash_rounding
     account_invoice
+    purchase
 xml:
     account.xml
+    message.xml
 
 [register]
 model:
@@ -18,3 +21,7 @@
 model:
     edocument.Invoice
     account.InvoiceEdocumentStart
+
+[register account_invoice purchase]
+model:
+    edocument.Invoice_Purchase


Reply via email to