Render allowance and charge in UBL invoice
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Render allowance and charge
|
||||
* Fill buyer's item identification
|
||||
|
||||
Version 7.8.0 - 2025-12-15
|
||||
|
||||
@@ -30,6 +30,9 @@ from .exceptions import InvoiceError
|
||||
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 @@ class Invoice(Model):
|
||||
|
||||
@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):
|
||||
|
||||
@@ -65,6 +65,17 @@ this repository contains the full copyright notices and license terms. -->
|
||||
</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 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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 @@ this repository contains the full copyright notices and license terms. -->
|
||||
</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>
|
||||
|
||||
@@ -65,6 +65,17 @@ this repository contains the full copyright notices and license terms. -->
|
||||
</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 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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 @@ this repository contains the full copyright notices and license terms. -->
|
||||
</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>
|
||||
|
||||
@@ -77,4 +77,11 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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>
|
||||
|
||||
@@ -79,16 +79,22 @@ def get_invoice():
|
||||
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 @@ def get_invoice():
|
||||
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 @@ def get_invoice():
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user