[IMP] account_edi_ubl_cii: handle payment terms

Handle payment terms for UBL as described by the Belgian Peppol
Authority and documented by VBO-FEB.

In case of early payment discount (for instance 2% if payment within 7
days):

* add one document level allowance per VAT rate (this allows to reduce
the tax base amount to take the discount into account)

* add one document level charge exempt from VAT (the amount of this
charge is the amount of the discount the customer can get by paying
early)

* Recompute the document level TaxTotal accordingly

Hence, nothing changes for the invoice lines. Importing the xml is then
convenient since each document level AllowanceCharge will create an
invoice line on top of the existing invoice lines. The total amount of
the invoice will be the amount to pay in case of no early payment
discount. To get the total amount to pay in case of early payment
discount, simply remove the document level charge.

References:
https://openpeppol.atlassian.net/wiki/spaces/Belgium/overview
https://efactuur.belgium.be/sites/default/files/3.%20Cash%20discount.docx

opw-3235769
task-3326963

closes odoo/odoo#136635

X-original-commit: 80bb997d7ecab208e6ca9719114cf9ae3c31f505
Signed-off-by: Laurent Smet (las) <las@odoo.com>
Signed-off-by: Julien Van Roy (juvr) <juvr@odoo.com>
This commit is contained in:
Julien Van Roy
2023-09-28 15:48:27 +00:00
parent ba3d6356ab
commit 86a84afc69
26 changed files with 611 additions and 32 deletions
@@ -386,6 +386,9 @@
<cbc:AllowanceTotalAmount
t-att-currencyID="monetary_tot_vals['currency'].name"
t-out="format_float(monetary_tot_vals.get('allowance_total_amount'), monetary_tot_vals.get('currency_dp'))"/>
<cbc:ChargeTotalAmount
t-att-currencyID="monetary_tot_vals['currency'].name"
t-out="format_float(monetary_tot_vals.get('charge_total_amount'), monetary_tot_vals.get('currency_dp'))"/>
<cbc:PrepaidAmount
t-att-currencyID="monetary_tot_vals['currency'].name"
t-out="format_float(monetary_tot_vals.get('prepaid_amount'), monetary_tot_vals.get('currency_dp'))"/>
@@ -528,6 +528,14 @@ msgstr ""
msgid "Contact"
msgstr ""
#. module: account_edi_ubl_cii
#. odoo-python
#: code:addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py:0
#: code:addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py:0
#, python-format
msgid "Conditional cash/payment discount"
msgstr ""
#. module: account_edi_ubl_cii
#. odoo-python
#: code:addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py:0
@@ -390,12 +390,9 @@ class AccountEdiCommon(models.AbstractModel):
# get the name
name = ""
reason_code_node = allow_el.find('./{*}AllowanceChargeReasonCode' if is_ubl else './{*}ReasonCode')
if reason_code_node is not None:
name += reason_code_node.text + " "
reason_node = allow_el.find('./{*}AllowanceChargeReason' if is_ubl else './{*}Reason')
if reason_node is not None:
name += reason_node.text
name = reason_node.text
# get quantity and price unit
quantity = 1
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
from lxml import etree
from odoo import models, _
@@ -162,7 +163,8 @@ class AccountEdiXmlUBL20(models.AbstractModel):
def _get_invoice_payment_terms_vals_list(self, invoice):
payment_term = invoice.invoice_payment_term_id
if payment_term:
return [{'note_vals': [payment_term.name]}]
# The payment term's note is automatically embedded in a <p> tag in Odoo
return [{'note_vals': [html2plaintext(payment_term.note)]}]
else:
return []
@@ -173,16 +175,30 @@ class AccountEdiXmlUBL20(models.AbstractModel):
'tax_amount': taxes_vals['tax_amount_currency'],
'tax_subtotal_vals': [],
}
epd_tax_to_discount = self._get_early_payment_discount_grouped_by_tax_rate(invoice)
for grouping_key, vals in taxes_vals['tax_details'].items():
if grouping_key['tax_amount_type'] != 'fixed':
tax_totals_vals['tax_subtotal_vals'].append({
subtotal = {
'currency': invoice.currency_id,
'currency_dp': self._get_currency_decimal_places(invoice.currency_id),
'taxable_amount': vals['base_amount_currency'],
'tax_amount': vals['tax_amount_currency'],
'percent': vals['_tax_category_vals_']['percent'],
'tax_category_vals': vals['_tax_category_vals_'],
})
}
if epd_tax_to_discount:
# early payment discounts: need to recompute the tax/taxable amounts
taxable_amount_after_epd = subtotal['taxable_amount'] - epd_tax_to_discount.get(subtotal['percent'], 0)
tax_amount_after_epd = taxable_amount_after_epd * subtotal['tax_category_vals']['percent'] / 100
subtotal.update({
'taxable_amount': taxable_amount_after_epd,
'tax_amount': tax_amount_after_epd,
})
tax_totals_vals['tax_subtotal_vals'].append(subtotal)
if epd_tax_to_discount:
# early payment discounts: hence, need to recompute the total tax amount
tax_totals_vals['tax_amount'] = sum([subtot['tax_amount'] for subtot in tax_totals_vals['tax_subtotal_vals']])
return [tax_totals_vals]
def _get_invoice_line_item_vals(self, line, taxes_vals):
@@ -216,8 +232,45 @@ class AccountEdiXmlUBL20(models.AbstractModel):
def _get_document_allowance_charge_vals_list(self, invoice):
"""
https://docs.peppol.eu/poacc/billing/3.0/bis/#_document_level_allowance_or_charge
Usage for early payment discounts:
* Add one document level Allowance per tax rate (VAT included)
* Add one document level Charge (VAT excluded) with amount = the total sum of the early payment discount
The difference between these is the cash discount in case of early payment.
"""
return []
vals_list = []
# Early Payment Discount
epd_tax_to_discount = self._get_early_payment_discount_grouped_by_tax_rate(invoice)
if epd_tax_to_discount:
# One Allowance per tax rate (VAT included)
for tax_amount, discount_amount in epd_tax_to_discount.items():
vals_list.append({
'charge_indicator': 'false',
'allowance_charge_reason_code': '66',
'allowance_charge_reason': _("Conditional cash/payment discount"),
'amount': discount_amount,
'currency_dp': 2,
'currency_name': invoice.currency_id.name,
'tax_category_vals': [{
'id': 'S',
'percent': tax_amount,
'tax_scheme_id': 'VAT',
}],
})
# One global Charge (VAT exempted)
vals_list.append({
'charge_indicator': 'true',
'allowance_charge_reason_code': 'ZZZ',
'allowance_charge_reason': _("Conditional cash/payment discount"),
'amount': sum(epd_tax_to_discount.values()),
'currency_dp': 2,
'currency_name': invoice.currency_id.name,
'tax_category_vals': [{
'id': 'E',
'percent': 0.0,
'tax_scheme_id': 'VAT',
}],
})
return vals_list
def _get_invoice_line_allowance_vals_list(self, line, tax_values_list=None):
""" Method used to fill the cac:InvoiceLine>cac:AllowanceCharge node.
@@ -332,7 +385,7 @@ class AccountEdiXmlUBL20(models.AbstractModel):
'price_vals': self._get_invoice_line_price_vals(line),
}
def _get_invoice_legal_monetary_total_vals(self, invoice, taxes_vals, line_extension_amount, allowance_total_amount):
def _get_invoice_legal_monetary_total_vals(self, invoice, taxes_vals, line_extension_amount, allowance_total_amount, charge_total_amount):
""" Method used to fill the cac:LegalMonetaryTotal node"""
return {
'currency': invoice.currency_id,
@@ -341,6 +394,7 @@ class AccountEdiXmlUBL20(models.AbstractModel):
'tax_exclusive_amount': taxes_vals['base_amount_currency'],
'tax_inclusive_amount': invoice.amount_total,
'allowance_total_amount': allowance_total_amount or None,
'charge_total_amount': charge_total_amount or None,
'prepaid_amount': invoice.amount_total - invoice.amount_residual,
'payable_amount': invoice.amount_residual,
}
@@ -357,6 +411,19 @@ class AccountEdiXmlUBL20(models.AbstractModel):
"""
return True
def _get_early_payment_discount_grouped_by_tax_rate(self, invoice):
"""
Get the early payment discounts grouped by the tax rate of the product it is linked to
:returns {float: float}: mapping tax amounts to early payment discount amounts
"""
if invoice.invoice_payment_term_id.early_pay_discount_computation != 'mixed':
return {}
tax_to_discount = defaultdict(lambda: 0)
for line in invoice.line_ids.filtered(lambda l: l.display_type == 'epd'):
for tax in line.tax_ids:
tax_to_discount[tax.amount] += line.amount_currency
return tax_to_discount
def _export_invoice_vals(self, invoice):
def grouping_key_generator(base_line, tax_values):
tax = tax_values['tax_repartition_line'].tax_id
@@ -410,9 +477,12 @@ class AccountEdiXmlUBL20(models.AbstractModel):
# Compute the total allowance/charge amounts.
allowance_total_amount = 0.0
charge_total_amount = 0.0
for allowance_charge_vals in document_allowance_charge_vals_list:
if allowance_charge_vals['charge_indicator'] == 'false':
allowance_total_amount += allowance_charge_vals['amount']
else:
charge_total_amount += allowance_charge_vals['amount']
supplier = invoice.company_id.partner_id.commercial_partner_id
customer = invoice.commercial_partner_id
@@ -463,7 +533,13 @@ class AccountEdiXmlUBL20(models.AbstractModel):
# allowances at the document level, the allowances on invoices (eg. discount) are on invoice_line_vals
'allowance_charge_vals': document_allowance_charge_vals_list,
'tax_total_vals': self._get_invoice_tax_totals_vals_list(invoice, taxes_vals),
'legal_monetary_total_vals': self._get_invoice_legal_monetary_total_vals(invoice, taxes_vals, line_extension_amount, allowance_total_amount),
'legal_monetary_total_vals': self._get_invoice_legal_monetary_total_vals(
invoice,
taxes_vals,
line_extension_amount,
allowance_total_amount,
charge_total_amount,
),
'invoice_line_vals': invoice_line_vals_list,
'currency_dp': self._get_currency_decimal_places(invoice.currency_id), # currency decimal places
},
@@ -577,11 +653,11 @@ class AccountEdiXmlUBL20(models.AbstractModel):
narration = ""
note_node = tree.find('./{*}Note')
if note_node is not None and note_node.text:
narration += note_node.text + "\n"
narration += f"<p>{note_node.text}</p>"
payment_terms_node = tree.find('./{*}PaymentTerms/{*}Note') # e.g. 'Payment within 10 days, 2% discount'
if payment_terms_node is not None and payment_terms_node.text:
narration += payment_terms_node.text + "\n"
narration += f"<p>{payment_terms_node.text}</p>"
invoice.narration = narration
@@ -99,7 +99,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">268.20</cbc:TaxAmount>
@@ -98,7 +98,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">268.20</cbc:TaxAmount>
@@ -100,7 +100,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">21.00</cbc:TaxAmount>
@@ -100,7 +100,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">21.00</cbc:TaxAmount>
@@ -100,7 +100,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">21.00</cbc:TaxAmount>
@@ -97,7 +97,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">482.22</cbc:TaxAmount>
@@ -78,7 +78,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">482.22</cbc:TaxAmount>
@@ -91,7 +91,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">42.00</cbc:TaxAmount>
@@ -102,7 +102,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">959.07</cbc:TaxAmount>
@@ -96,7 +96,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">482.22</cbc:TaxAmount>
@@ -0,0 +1,218 @@
<?xml version='1.0' encoding='UTF-8'?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>___ignore___</cbc:ID>
<cbc:IssueDate>2017-01-01</cbc:IssueDate>
<cbc:DueDate>2017-01-31</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:Note>test narration</cbc:Note>
<cbc:DocumentCurrencyCode>USD</cbc:DocumentCurrencyCode>
<cbc:BuyerReference>ref_partner_2</cbc:BuyerReference>
<cac:OrderReference>
<cbc:ID>ref_move</cbc:ID>
</cac:OrderReference>
<cac:AdditionalDocumentReference>
<cbc:ID>___ignore___</cbc:ID>
<cbc:DocumentTypeCode>380</cbc:DocumentTypeCode>
<cbc:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="___ignore___" filename="___ignore___">
___ignore___
</cbc:EmbeddedDocumentBinaryObject>
</cbc:Attachment>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>partner_1</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Chaussée de Namur 40</cbc:StreetName>
<cbc:CityName>Ramillies</cbc:CityName>
<cbc:PostalZone>1367</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0202239951</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>partner_1</cbc:RegistrationName>
<cbc:CompanyID>BE0202239951</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>partner_1</cbc:Name>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>partner_2</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Rue des Bourlottes 9</cbc:StreetName>
<cbc:CityName>Ramillies</cbc:CityName>
<cbc:PostalZone>1367</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0477472701</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>partner_2</cbc:RegistrationName>
<cbc:CompanyID>BE0477472701</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>partner_2</cbc:Name>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:Delivery>
<cac:DeliveryLocation>
<cac:Address>
<cbc:StreetName>Rue des Bourlottes 9</cbc:StreetName>
<cbc:CityName>Ramillies</cbc:CityName>
<cbc:PostalZone>1367</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="credit transfer">30</cbc:PaymentMeansCode>
<cbc:PaymentID>___ignore___</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>BE15001559627230</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>Payment terms: 30 Days, 2% Early Payment Discount under 7 days</cbc:Note>
</cac:PaymentTerms>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>66</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>Conditional cash/payment discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="USD">4.00</cbc:Amount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>6.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>66</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>Conditional cash/payment discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="USD">48.00</cbc:Amount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>ZZZ</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>Conditional cash/payment discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="USD">52.00</cbc:Amount>
<cac:TaxCategory>
<cbc:ID>E</cbc:ID>
<cbc:Percent>0.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">505.68</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="USD">196.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="USD">11.76</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>6.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="USD">2352.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="USD">493.92</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="USD">2600.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="USD">2600.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="USD">3105.68</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount currencyID="USD">52.00</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount currencyID="USD">52.00</cbc:ChargeTotalAmount>
<cbc:PrepaidAmount currencyID="USD">0.00</cbc:PrepaidAmount>
<cbc:PayableAmount currencyID="USD">3105.68</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>___ignore___</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">200.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_a</cbc:Description>
<cbc:Name>product_a</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>6.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="USD">200.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>___ignore___</cbc:ID>
<cbc:InvoicedQuantity unitCode="DZN">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">2400.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_b</cbc:Description>
<cbc:Name>product_b</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="USD">2400.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
@@ -0,0 +1,181 @@
<?xml version='1.0' encoding='UTF-8'?>
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0
</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>___ignore___</cbc:ID>
<cbc:IssueDate>2017-01-01</cbc:IssueDate>
<cbc:DueDate>2017-01-31</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:Note>test narration</cbc:Note>
<cbc:DocumentCurrencyCode>USD</cbc:DocumentCurrencyCode>
<cbc:BuyerReference>ref_partner_2</cbc:BuyerReference>
<cac:OrderReference>
<cbc:ID>ref_move</cbc:ID>
</cac:OrderReference>
<cac:AdditionalDocumentReference>
<cbc:ID>___ignore___</cbc:ID>
<cbc:DocumentTypeCode>380</cbc:DocumentTypeCode>
<cbc:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="___ignore___" filename="___ignore___">
___ignore___
</cbc:EmbeddedDocumentBinaryObject>
</cbc:Attachment>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0202239951</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>partner_1</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Chaussée de Namur 40</cbc:StreetName>
<cbc:CityName>Ramillies</cbc:CityName>
<cbc:PostalZone>1367</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0202239951</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>partner_1</cbc:RegistrationName>
<cbc:CompanyID>BE0202239951</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>partner_1</cbc:Name>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0208">0477472701</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>partner_2</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Rue des Bourlottes 9</cbc:StreetName>
<cbc:CityName>Ramillies</cbc:CityName>
<cbc:PostalZone>1367</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>BE0477472701</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>partner_2</cbc:RegistrationName>
<cbc:CompanyID>BE0477472701</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>partner_2</cbc:Name>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:Delivery>
<cac:DeliveryLocation>
<cac:Address>
<cbc:StreetName>Rue des Bourlottes 9</cbc:StreetName>
<cbc:CityName>Ramillies</cbc:CityName>
<cbc:PostalZone>1367</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>BE</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="credit transfer">30</cbc:PaymentMeansCode>
<cbc:PaymentID>___ignore___</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>BE15001559627230</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>Payment terms: 30 Days, 2% Early Payment Discount under 7 days</cbc:Note>
</cac:PaymentTerms>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>false</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>66</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>Conditional cash/payment discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="USD">1.98</cbc:Amount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>ZZZ</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>Conditional cash/payment discount</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="USD">1.98</cbc:Amount>
<cac:TaxCategory>
<cbc:ID>E</cbc:ID>
<cbc:Percent>0.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:AllowanceCharge>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">20.58</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="USD">98.02</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="USD">20.58</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="USD">100.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="USD">100.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="USD">120.58</cbc:TaxInclusiveAmount>
<cbc:AllowanceTotalAmount currencyID="USD">1.98</cbc:AllowanceTotalAmount>
<cbc:ChargeTotalAmount currencyID="USD">1.98</cbc:ChargeTotalAmount>
<cbc:PrepaidAmount currencyID="USD">0.00</cbc:PrepaidAmount>
<cbc:PayableAmount currencyID="USD">120.58</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>___ignore___</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="USD">100.00</cbc:LineExtensionAmount>
<cac:AllowanceCharge>
<cbc:ChargeIndicator>true</cbc:ChargeIndicator>
<cbc:AllowanceChargeReasonCode>AEO</cbc:AllowanceChargeReasonCode>
<cbc:AllowanceChargeReason>RECUPEL</cbc:AllowanceChargeReason>
<cbc:Amount currencyID="USD">1.00</cbc:Amount>
</cac:AllowanceCharge>
<cac:Item>
<cbc:Description>product_a</cbc:Description>
<cbc:Name>product_a</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="USD">99.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
@@ -105,7 +105,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">401.58</cbc:TaxAmount>
@@ -109,7 +109,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">401.58</cbc:TaxAmount>
@@ -75,7 +75,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">-0.00</cbc:TaxAmount>
@@ -99,7 +99,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">401.58</cbc:TaxAmount>
@@ -98,7 +98,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="USD">401.58</cbc:TaxAmount>
@@ -69,8 +69,26 @@ class TestUBLBE(TestUBLCommon, TestAccountMoveSendCommon):
'country_id': cls.env.ref('base.be').id,
})
cls.tax_6 = cls.env['account.tax'].create({
'name': 'tax_6',
'amount_type': 'percent',
'amount': 6,
'type_tax_use': 'sale',
'country_id': cls.env.ref('base.be').id,
})
cls.env.company.invoice_is_ubl_cii = True
cls.pay_term = cls.env['account.payment.term'].create({
'name': "2/7 Net 30",
'note': "Payment terms: 30 Days, 2% Early Payment Discount under 7 days",
'early_discount': True,
'discount_percentage': 2,
'discount_days': 7,
'line_ids': [
Command.create({'value': 'percent', 'value_amount': 100.0, 'nb_days': 30})],
})
@classmethod
def setup_company_data(cls, company_name, chart_template):
# OVERRIDE
@@ -341,6 +359,72 @@ class TestUBLBE(TestUBLCommon, TestAccountMoveSendCommon):
self.assertEqual(invoice.amount_total, 121)
self._assert_invoice_attachment(invoice.ubl_cii_xml_id, None, 'from_odoo/bis3_ecotaxes_case3.xml')
def test_export_payment_terms(self):
"""
Tests the early payment discount using the example case from the VBO/FEB.
------------- + Price + Tax + Cash Discount (2%) + Taxable Amount + VAT --
Product A | 200 | 6% | -4 | 196 | 11.76
Product B | 2400 | 21% | -48 | 2352 | 493.92
--------------+-------+-----+--------------------+----------------+-------
Subtotal (Taxable amount incl. payment discount): 2548
VAT: 505.68
Payable amount (excl. payment discount): 3105.68
Payable amount (incl. payment discount): 3053.68
"""
invoice = self._generate_move(
self.partner_1,
self.partner_2,
move_type='out_invoice',
invoice_payment_term_id=self.pay_term.id,
invoice_line_ids=[
{
'product_id': self.product_a.id,
'quantity': 1,
'price_unit': 200,
'tax_ids': [(6, 0, [self.tax_6.id])],
},
{
'product_id': self.product_b.id,
'quantity': 1,
'price_unit': 2400,
'tax_ids': [(6, 0, [self.tax_21.id])],
}
],
)
self._assert_invoice_attachment(invoice.ubl_cii_xml_id, None, 'from_odoo/bis3_pay_term.xml')
def test_export_payment_terms_fixed_tax(self):
"""
Tests the early payment discount combined with a fixed tax.
------------- + Price + Tax + Cash Discount (2%) + ------- Taxable Amount + VAT ----
Product A | 99 | 21% | -1.98 | 97.02 + 1 (fixed tax) | 20.5842
--------------+-------+-----+--------------------+------------------------+---------
NB: The fixed taxes (recupel, auvibel, etc) are excluded from the early payment discount !
Subtotal (Taxable amount incl. payment discount): 97.02 + 1
VAT: (97.02 + 1) * 0.21 = 20.58
Payable amount (excl. payment discount): 99 + 1 + 20.58 = 120.58
Payable amount (incl. payment discount): 97.02 + 1 + 20.58 = 118.60
"""
invoice = self._generate_move(
self.partner_1,
self.partner_2,
move_type='out_invoice',
invoice_payment_term_id=self.pay_term.id,
invoice_line_ids=[
{
'product_id': self.product_a.id,
'quantity': 1,
'price_unit': 99,
'tax_ids': [(6, 0, [self.tax_21.id, self.recupel.id])],
},
],
)
self._assert_invoice_attachment(invoice.ubl_cii_xml_id, None, 'from_odoo/bis3_pay_term_ecotax.xml')
####################################################
# Test import
####################################################
@@ -511,6 +595,18 @@ class TestUBLBE(TestUBLCommon, TestAccountMoveSendCommon):
list_line_discount=[0], list_line_taxes=[tax_21+self.recupel], move_type='out_invoice',
)
def test_import_payment_terms(self):
# The tax 21% from l10n_be is retrieved since it's a duplicate of self.tax_21
tax_21 = self.env.ref(f'account.{self.env.company.id}_attn_VAT-OUT-21-L')
tax_0 = self.env.ref(f'account.{self.env.company.id}_attn_VAT-OUT-00-L')
self._assert_imported_invoice_from_file(
subfolder='tests/test_files/from_odoo', filename='bis3_pay_term.xml', amount_total=3105.68,
amount_tax=505.68, list_line_subtotals=[-4, -48, 52, 200, 2400],
currency_id=self.currency_data['currency'].id, list_line_price_unit=[-4, -48, 52, 200, 2400],
list_line_discount=[0, 0, 0, 0, 0], list_line_taxes=[self.tax_6, tax_21, tax_0, self.tax_6, tax_21],
move_type='out_invoice',
)
####################################################
# Test Send & print
####################################################
@@ -199,9 +199,9 @@ class AccountEdiXmlOIOUBL201(models.AbstractModel):
return vals_list
def _get_invoice_legal_monetary_total_vals(self, invoice, taxes_vals, line_extension_amount, allowance_total_amount):
def _get_invoice_legal_monetary_total_vals(self, invoice, taxes_vals, line_extension_amount, allowance_total_amount, charge_total_amount):
# EXTENDS account.edi.xml.ubl_20
vals = super()._get_invoice_legal_monetary_total_vals(invoice, taxes_vals, line_extension_amount, allowance_total_amount)
vals = super()._get_invoice_legal_monetary_total_vals(invoice, taxes_vals, line_extension_amount, allowance_total_amount, charge_total_amount)
# In OIOUBL context, tax_exclusive_amount means "tax only"
vals['tax_exclusive_amount'] = taxes_vals['tax_amount_currency']
if invoice.currency_id.is_zero(vals['prepaid_amount']):
@@ -103,7 +103,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="RON">285.00</cbc:TaxAmount>
@@ -103,7 +103,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="FUS">285.00</cbc:TaxAmount>
@@ -102,7 +102,7 @@
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>30% Advance End of Following Month</cbc:Note>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="RON">285.00</cbc:TaxAmount>