details: https://code.tryton.org/tryton/commit/69beb4452faa
branch: default
user: Cédric Krier <[email protected]>
date: Mon Jan 26 15:38:06 2026 +0100
description:
Render allowance and charge in UBL invoice
diffstat:
modules/edocument_ubl/CHANGELOG | 1 +
modules/edocument_ubl/edocument.py | 28 ++++++++++++++++++++++++-
modules/edocument_ubl/template/2/CreditNote.xml | 23 +++++++++++--------
modules/edocument_ubl/template/2/Invoice.xml | 23 +++++++++++--------
modules/edocument_ubl/template/2/base.xml | 7 ++++++
modules/edocument_ubl/tests/test_module.py | 28 ++++++++++++++++++------
6 files changed, 82 insertions(+), 28 deletions(-)
diffs (228 lines):
diff -r 51d3e0bcd5fb -r 69beb4452faa modules/edocument_ubl/CHANGELOG
--- a/modules/edocument_ubl/CHANGELOG Mon Jan 26 15:42:27 2026 +0100
+++ b/modules/edocument_ubl/CHANGELOG Mon Jan 26 15:38:06 2026 +0100
@@ -1,3 +1,4 @@
+* Render allowance and charge
* Fill buyer's item identification
Version 7.8.0 - 2025-12-15
diff -r 51d3e0bcd5fb -r 69beb4452faa modules/edocument_ubl/edocument.py
--- a/modules/edocument_ubl/edocument.py Mon Jan 26 15:42:27 2026 +0100
+++ b/modules/edocument_ubl/edocument.py Mon Jan 26 15:38:06 2026 +0100
@@ -30,6 +30,9 @@
from .party import ISO6523_TYPES
ISO6523 = {v: k for k, v in ISO6523_TYPES.items()}
+PEPPOL_ALLOWANCES = {
+ '41', '42', '60', '62', '63', '64', '65', '66', '67', '68', '70', '71',
+ '88', '95', '100', '102', '103', '104', '105'}
if not hasattr(ASTCodeGenerator, 'visit_NameConstant'):
def visit_NameConstant(self, node):
@@ -217,7 +220,30 @@
@cached_property
def lines(self):
- return [l for l in self.invoice.lines if l.type == 'line']
+ return [
+ l for l in self.invoice.lines
+ if l.type == 'line' and not self.allowance_charge_code(l)]
+
+ @cached_property
+ def allowance_charge(self):
+ return [
+ l for l in self.invoice.lines
+ if l.type == 'line' and self.allowance_charge_code(l)]
+
+ @classmethod
+ def allowance_charge_code(cls, line, specification=None):
+ if product := line.product:
+ sequence_type = line.invoice.sequence_type
+ if ((line.quantity > 0 and sequence_type == 'invoice')
+ or (line.quantity < 0 and sequence_type == 'credit_note')):
+ return product.unece_special_service_code
+ elif ((line.quantity < 0 and sequence_type == 'invoice')
+ or (line.quantity < 0 and sequence_type == 'credit_note')):
+ if (specification or '').startswith('peppol'):
+ if (product.unece_allowance_charge_code
+ not in PEPPOL_ALLOWANCES):
+ return
+ return product.unece_allowance_charge_code
@classmethod
def parse(cls, document):
diff -r 51d3e0bcd5fb -r 69beb4452faa
modules/edocument_ubl/template/2/CreditNote.xml
--- a/modules/edocument_ubl/template/2/CreditNote.xml Mon Jan 26 15:42:27
2026 +0100
+++ b/modules/edocument_ubl/template/2/CreditNote.xml Mon Jan 26 15:38:06
2026 +0100
@@ -65,6 +65,17 @@
</cac:PaymentTerms>
</py:otherwise>
</py:choose>
+<cac:AllowanceCharge py:for="allowance_charge in this.allowance_charge">
+ <cbc:ChargeIndicator>${'false' if allowance_charge.quantity > 0 else
'true'}</cbc:ChargeIndicator>
+ <py:with vars="reason_code=this.allowance_charge_code(allowance_charge,
specification=specification)">
+ <cbc:AllowanceChargeReasonCode
py:if="reason_code">${reason_code}</cbc:AllowanceChargeReasonCode>
+ </py:with>
+
<cbc:AllowanceChargeReason>${allowance_charge.product.name}</cbc:AllowanceChargeReason>
+ <cbc:Amount py:attrs="{'currencyID':
this.invoice.currency.code}">${abs(allowance_charge.amount)}</cbc:Amount>
+ <cac:TaxCategory py:for="tax in allowance_charge.taxes">
+ ${TaxCategory(tax)}
+ </cac:TaxCategory>
+</cac:AllowanceCharge>
<cac:TaxTotal py:for="group, lines, amount in this.taxes">
<cbc:TaxAmount py:attrs="{'currencyID':
this.invoice.currency.code}">${-amount}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
@@ -72,11 +83,7 @@
<cbc:TaxAmount py:attrs="{'currencyID':
this.invoice.currency.code}">${-line.amount}</cbc:TaxAmount>
<cbc:Percent py:if="line.tax.type == 'percentage' and not
(specification or '').startswith('peppol')">${format((line.tax.rate *
100).normalize(), 'f')}</cbc:Percent>
<cac:TaxCategory>
- <cbc:ID
py:if="line.tax.unece_category_code">${line.tax.unece_category_code}</cbc:ID>
- <cbc:Percent py:if="line.tax.type ==
'percentage'">${format((line.tax.rate * 100).normalize(), 'f')}</cbc:Percent>
- <cac:TaxScheme py:if="line.tax.unece_code">
- <cbc:ID>${line.tax.unece_code}</cbc:ID>
- </cac:TaxScheme>
+ ${TaxCategory(line.tax)}
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
@@ -110,11 +117,7 @@
</py:with>
</py:if>
<cac:ClassifiedTaxCategory py:for="tax in line.taxes">
- <cbc:ID
py:if="tax.unece_category_code">${tax.unece_category_code}</cbc:ID>
- <cbc:Percent py:if="tax.type == 'percentage'">${format((tax.rate *
100).normalize(), 'f')}</cbc:Percent>
- <cac:TaxScheme py:if="tax.unece_code">
- <cbc:ID>${tax.unece_code}</cbc:ID>
- </cac:TaxScheme>
+ ${TaxCategory(tax)}
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
diff -r 51d3e0bcd5fb -r 69beb4452faa
modules/edocument_ubl/template/2/Invoice.xml
--- a/modules/edocument_ubl/template/2/Invoice.xml Mon Jan 26 15:42:27
2026 +0100
+++ b/modules/edocument_ubl/template/2/Invoice.xml Mon Jan 26 15:38:06
2026 +0100
@@ -65,6 +65,17 @@
</cac:PaymentTerms>
</py:otherwise>
</py:choose>
+<cac:AllowanceCharge py:for="allowance_charge in this.allowance_charge">
+ <cbc:ChargeIndicator>${'true' if allowance_charge.quantity > 0 else
'false'}</cbc:ChargeIndicator>
+ <py:with vars="reason_code=this.allowance_charge_code(allowance_charge,
specification=specification)">
+ <cbc:AllowanceChargeReasonCode
py:if="reason_code">${reason_code}</cbc:AllowanceChargeReasonCode>
+ </py:with>
+
<cbc:AllowanceChargeReason>${allowance_charge.product.name}</cbc:AllowanceChargeReason>
+ <cbc:Amount py:attrs="{'currencyID':
this.invoice.currency.code}">${abs(allowance_charge.amount)}</cbc:Amount>
+ <cac:TaxCategory py:for="tax in allowance_charge.taxes">
+ ${TaxCategory(tax)}
+ </cac:TaxCategory>
+</cac:AllowanceCharge>
<cac:TaxTotal py:for="group, lines, amount in this.taxes">
<cbc:TaxAmount py:attrs="{'currencyID':
this.invoice.currency.code}">${amount}</cbc:TaxAmount>
<cac:TaxSubtotal py:for="line in lines">
@@ -72,11 +83,7 @@
<cbc:TaxAmount py:attrs="{'currencyID':
this.invoice.currency.code}">${line.amount}</cbc:TaxAmount>
<cbc:Percent py:if="line.tax.type == 'percentage' and not
(specification or '').startswith('peppol')">${format((line.tax.rate *
100).normalize(), 'f')}</cbc:Percent>
<cac:TaxCategory>
- <cbc:ID
py:if="line.tax.unece_category_code">${line.tax.unece_category_code}</cbc:ID>
- <cbc:Percent py:if="line.tax.type ==
'percentage'">${format((line.tax.rate * 100).normalize(), 'f')}</cbc:Percent>
- <cac:TaxScheme py:if="line.tax.unece_code">
- <cbc:ID>${line.tax.unece_code}</cbc:ID>
- </cac:TaxScheme>
+ ${TaxCategory(line.tax)}
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
@@ -110,11 +117,7 @@
</py:with>
</py:if>
<cac:ClassifiedTaxCategory py:for="tax in line.taxes">
- <cbc:ID
py:if="tax.unece_category_code">${tax.unece_category_code}</cbc:ID>
- <cbc:Percent py:if="tax.type == 'percentage'">${format((tax.rate *
100).normalize(), 'f')}</cbc:Percent>
- <cac:TaxScheme py:if="tax.unece_code">
- <cbc:ID>${tax.unece_code}</cbc:ID>
- </cac:TaxScheme>
+ ${TaxCategory(tax)}
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
diff -r 51d3e0bcd5fb -r 69beb4452faa modules/edocument_ubl/template/2/base.xml
--- a/modules/edocument_ubl/template/2/base.xml Mon Jan 26 15:42:27 2026 +0100
+++ b/modules/edocument_ubl/template/2/base.xml Mon Jan 26 15:38:06 2026 +0100
@@ -77,4 +77,11 @@
<cbc:Name py:if="not (specification or
'').startswith('peppol')">${address.country.name}</cbc:Name>
</cac:Country>
</py:def>
+ <py:def function="TaxCategory(tax)">
+ <cbc:ID
py:if="tax.unece_category_code">${tax.unece_category_code}</cbc:ID>
+ <cbc:Percent py:if="tax.type == 'percentage'">${format((tax.rate *
100).normalize(), 'f')}</cbc:Percent>
+ <cac:TaxScheme py:if="tax.unece_code">
+ <cbc:ID>${tax.unece_code}</cbc:ID>
+ </cac:TaxScheme>
+ </py:def>
</py:strip>
diff -r 51d3e0bcd5fb -r 69beb4452faa modules/edocument_ubl/tests/test_module.py
--- a/modules/edocument_ubl/tests/test_module.py Mon Jan 26 15:42:27
2026 +0100
+++ b/modules/edocument_ubl/tests/test_module.py Mon Jan 26 15:38:06
2026 +0100
@@ -79,16 +79,22 @@
type='percentage',
rate=Decimal('.1'),
),
- base=Decimal('100.00'),
- amount=Decimal('10.00'),
+ base=Decimal('105.00'),
+ amount=Decimal('10.50'),
legal_notice="Legal Notice",
)]
product = Mock(spec=Product,
code="12345",
type='service',
+ unece_special_service_code=None,
)
product.name = "Product"
product.identifier_get.return_value = '98412345678908'
+ charge = Mock(spec=Product,
+ type='service',
+ unece_special_service_code='ZZZ',
+ )
+ charge.name = "Charge"
lines = [Mock(spec=InvoiceLine,
type='line',
product=product,
@@ -99,6 +105,12 @@
amount=Decimal('100.00'),
description="Description",
taxes=[t.tax for t in taxes],
+ ), Mock(spec=Invoice,
+ type='line',
+ product=charge,
+ quantity=1,
+ amount=Decimal('5.00'),
+ taxes=[t.tax for t in taxes],
)]
invoice = MagicMock(spec=Invoice,
id=-1,
@@ -120,17 +132,19 @@
lines=lines,
line_lines=lines,
taxes=taxes,
- untaxed_amount=Decimal('100.00'),
- tax_amount=Decimal('10.00'),
- total_amount=Decimal('110.00'),
- amount_to_pay=Decimal('110.00'),
+ untaxed_amount=Decimal('105.00'),
+ tax_amount=Decimal('10.50'),
+ total_amount=Decimal('110.50'),
+ amount_to_pay=Decimal('110.50'),
lines_to_pay=[Mock(spec=MoveLine,
maturity_date=datetime.date.today(),
- amount=Decimal('110.00'),
+ amount=Decimal('110.50'),
)],
sales=[],
state='posted',
)
+ for line in invoice.lines:
+ line.invoice = invoice
return invoice