[FIX] {l10n_}account_edi_ubl_cii{_tests}: handle price_include taxes
In Factur-X, there is no way to represent a tax "price_include" because every amounts should be tax excluded. Currently in Factur-X, a line with a tax price_include = True will be incorrectly exported. Indeed, the Factur-X.xml is generated by setting the GrossPriceProduct as the price_unit. In Factur-X, this amount (and the others in the line details) should be tax excluded. Thus, it's wrong to set the GrossPriceProduct as the price_unit. To fix this, the GrossPriceProduct should be the price_unit if no tax price_include = True is set, otherwise, the gross price = price_unit/(1+tax/100). This way, the Factur-X file will be consistent with the norm. Note that the import of a Factur-X xml will thus try to pick taxes with price_include = False, and the price_unit will be tax excluded. If no matching tax with price_include = False is retrieved, a tax with price_include = True is searched, if found, the price_unit is recomputed accordingly. In both cases, the lines subtotals are the same. opw-3032382 closes odoo/odoo#106718 X-original-commit: 7f93f72a4f874e7073c0fec8ab0a89ebbd13e029 Signed-off-by: William André (wan) <wan@odoo.com> Signed-off-by: Julien Van Roy <juvr@odoo.com>
This commit is contained in:
@@ -31,9 +31,12 @@
|
||||
|
||||
<!-- Amounts. -->
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<!-- Line information: unit_price (discount is below the taxes) -->
|
||||
<!-- Line information: unit_price
|
||||
NB: the gross_price_unit should be the price tax excluded !
|
||||
if price_unit = 100 and tax 10% price_include -> then the gross_price_unit is 91
|
||||
-->
|
||||
<ram:GrossPriceProductTradePrice>
|
||||
<ram:ChargeAmount t-out="format_monetary(line.price_unit, 2)"/>
|
||||
<ram:ChargeAmount t-out="format_monetary(line_vals['gross_price_total_unit'], 2)"/>
|
||||
<!-- Discount. -->
|
||||
<ram:AppliedTradeAllowanceCharge t-if="line.discount">
|
||||
<ram:ChargeIndicator>
|
||||
@@ -66,17 +69,6 @@
|
||||
</ram:ApplicableTradeTax>
|
||||
</t>
|
||||
|
||||
<!-- Discount. -->
|
||||
<!-- A discount in percent can only be on the whole document (CalculationPercent), not on a line... -->
|
||||
<ram:SpecifiedTradeAllowanceCharge t-if="line.discount">
|
||||
<ram:ChargeIndicator>
|
||||
<udt:Indicator>false</udt:Indicator>
|
||||
</ram:ChargeIndicator>
|
||||
<ram:ActualAmount t-out="format_monetary(line.price_unit * line.discount/100, 2)"/>
|
||||
<!-- https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred5189.htm -->
|
||||
<ram:ReasonCode>95</ram:ReasonCode>
|
||||
</ram:SpecifiedTradeAllowanceCharge>
|
||||
|
||||
<!-- Subtotal. -->
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount t-out="format_monetary(line.price_subtotal, 2)"/>
|
||||
|
||||
@@ -408,7 +408,7 @@ class AccountEdiCommon(models.AbstractModel):
|
||||
|
||||
with (UBL | CII):
|
||||
* net_unit_price = 'Price/PriceAmount' | 'NetPriceProductTradePrice' (mandatory) (BT-146)
|
||||
* gross_unit_price = 'GrossPriceProductTradePrice' | 'GrossPriceProductTradePrice' (optional) (BT-148)
|
||||
* gross_unit_price = 'Price/AllowanceCharge/BaseAmount' | 'GrossPriceProductTradePrice' (optional) (BT-148)
|
||||
* basis_qty = 'Price/BaseQuantity' | 'BasisQuantity' (optional, either below net_price node or
|
||||
gross_price node) (BT-149)
|
||||
* billed_qty = 'InvoicedQuantity' | 'BilledQuantity' (mandatory) (BT-129)
|
||||
@@ -450,6 +450,12 @@ class AccountEdiCommon(models.AbstractModel):
|
||||
}
|
||||
:params: invoice_line
|
||||
:params: qty_factor
|
||||
:returns: {
|
||||
'quantity': float,
|
||||
'product_uom_id': (optional) uom.uom,
|
||||
'price_unit': float,
|
||||
'discount': float,
|
||||
}
|
||||
"""
|
||||
# basis_qty (optional)
|
||||
basis_qty = 1
|
||||
@@ -471,10 +477,9 @@ class AccountEdiCommon(models.AbstractModel):
|
||||
rebate_node = tree.find(xpath_dict['rebate'])
|
||||
net_price_unit_node = tree.find(xpath_dict['net_price_unit'])
|
||||
if rebate_node is not None:
|
||||
if net_price_unit_node is not None and gross_price_unit_node is not None:
|
||||
rebate = float(gross_price_unit_node.text) - float(net_price_unit_node.text)
|
||||
else:
|
||||
rebate = float(rebate_node.text)
|
||||
rebate = float(rebate_node.text)
|
||||
elif net_price_unit_node is not None and gross_price_unit_node is not None:
|
||||
rebate = float(gross_price_unit_node.text) - float(net_price_unit_node.text)
|
||||
|
||||
# net_price_unit (mandatory)
|
||||
net_price_unit = None
|
||||
@@ -519,9 +524,7 @@ class AccountEdiCommon(models.AbstractModel):
|
||||
####################################################
|
||||
|
||||
# quantity
|
||||
invoice_line.quantity = billed_qty * qty_factor
|
||||
if product_uom_id is not None:
|
||||
invoice_line.product_uom_id = product_uom_id
|
||||
quantity = billed_qty * qty_factor
|
||||
|
||||
# price_unit
|
||||
if gross_price_unit is not None:
|
||||
@@ -530,17 +533,57 @@ class AccountEdiCommon(models.AbstractModel):
|
||||
price_unit = (net_price_unit + rebate) / basis_qty
|
||||
else:
|
||||
raise UserError(_("No gross price nor net price found for line in xml"))
|
||||
invoice_line.price_unit = price_unit
|
||||
|
||||
# discount
|
||||
discount = 0
|
||||
if billed_qty * price_unit != 0 and price_subtotal is not None:
|
||||
invoice_line.discount = 100 * (1 - price_subtotal / (billed_qty * price_unit))
|
||||
discount = 100 * (1 - price_subtotal / (billed_qty * price_unit))
|
||||
|
||||
# Sometimes, the xml received is very bad: unit price = 0, qty = 1, but price_subtotal = -200
|
||||
# for instance, when filling a down payment as an invoice line. The equation in the docstring is not
|
||||
# respected, and the result will not be correct, so we just follow the simple rule below:
|
||||
if net_price_unit == 0 and price_subtotal != net_price_unit * (billed_qty / basis_qty) - allow_charge_amount:
|
||||
invoice_line.price_unit = price_subtotal / billed_qty
|
||||
price_unit = price_subtotal / billed_qty
|
||||
|
||||
return {
|
||||
'quantity': quantity,
|
||||
'price_unit': price_unit,
|
||||
'discount': discount,
|
||||
'product_uom_id': product_uom_id,
|
||||
}
|
||||
|
||||
def _import_fill_invoice_line_taxes(self, journal, tax_nodes, invoice_line_form, inv_line_vals, logs):
|
||||
# Taxes: all amounts are tax excluded, so first try to fetch price_include=False taxes,
|
||||
# if no results, try to fetch the price_include=True taxes. If results, need to adapt the price_unit.
|
||||
inv_line_vals['taxes'] = []
|
||||
for tax_node in tax_nodes:
|
||||
amount = float(tax_node.text)
|
||||
domain = [
|
||||
('company_id', '=', journal.company_id.id),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', journal.type),
|
||||
('amount', '=', amount),
|
||||
]
|
||||
tax_excl = self.env['account.tax'].search(domain + [('price_include', '=', False)], limit=1)
|
||||
tax_incl = self.env['account.tax'].search(domain + [('price_include', '=', True)], limit=1)
|
||||
if tax_excl:
|
||||
inv_line_vals['taxes'].append(tax_excl.id)
|
||||
elif tax_incl:
|
||||
inv_line_vals['taxes'].append(tax_incl.id)
|
||||
inv_line_vals['price_unit'] *= (1 + tax_incl.amount / 100)
|
||||
else:
|
||||
logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", amount, invoice_line_form.name))
|
||||
# Set the values on the line_form
|
||||
invoice_line_form.quantity = inv_line_vals['quantity']
|
||||
if inv_line_vals.get('product_uom_id'):
|
||||
invoice_line_form.product_uom_id = inv_line_vals['product_uom_id']
|
||||
else:
|
||||
logs.append(
|
||||
_("Could not retrieve the unit of measure for line with label '%s'.", invoice_line_form.name))
|
||||
invoice_line_form.price_unit = inv_line_vals['price_unit']
|
||||
invoice_line_form.discount = inv_line_vals['discount']
|
||||
invoice_line_form.tax_ids = inv_line_vals['taxes']
|
||||
return logs
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Check xml using the free API from Ph. Helger, don't abuse it !
|
||||
|
||||
@@ -332,30 +332,10 @@ class AccountEdiXmlCII(models.AbstractModel):
|
||||
'allowance_charge_amount': './{*}ActualAmount', # below allowance_charge node
|
||||
'line_total_amount': './{*}SpecifiedLineTradeSettlement/{*}SpecifiedTradeSettlementLineMonetarySummation/{*}LineTotalAmount',
|
||||
}
|
||||
self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor)
|
||||
|
||||
if not invoice_line.product_uom_id:
|
||||
logs.append(
|
||||
_("Could not retrieve the unit of measure for line with label '%s'. Did you install the inventory "
|
||||
"app and enabled the 'Units of Measure' option ?", invoice_line.name))
|
||||
|
||||
# Taxes
|
||||
taxes = self.env['account.tax']
|
||||
inv_line_vals = self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor)
|
||||
# retrieve tax nodes
|
||||
tax_nodes = tree.findall('.//{*}ApplicableTradeTax/{*}RateApplicablePercent')
|
||||
for tax_node in tax_nodes:
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', journal.company_id.id),
|
||||
('amount', '=', float(tax_node.text)),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', 'purchase'),
|
||||
], limit=1)
|
||||
if tax:
|
||||
taxes += tax
|
||||
else:
|
||||
logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", float(tax_node.text), invoice_line.name))
|
||||
|
||||
invoice_line.tax_ids = taxes
|
||||
return logs
|
||||
return self._import_fill_invoice_line_taxes(journal, tax_nodes, invoice_line, inv_line_vals, logs)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# IMPORT : helpers
|
||||
|
||||
@@ -586,33 +586,14 @@ class AccountEdiXmlUBL20(models.AbstractModel):
|
||||
}
|
||||
self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor)
|
||||
|
||||
if not invoice_line.product_uom_id:
|
||||
logs.append(
|
||||
_("Could not retrieve the unit of measure for line with label '%s'. Did you install the inventory "
|
||||
"app and enabled the 'Units of Measure' option ?", invoice_line.name))
|
||||
|
||||
# Taxes
|
||||
taxes = self.env['account.tax']
|
||||
|
||||
inv_line_vals = self._import_fill_invoice_line_values(tree, xpath_dict, invoice_line, qty_factor)
|
||||
# retrieve tax nodes
|
||||
tax_nodes = tree.findall('.//{*}Item/{*}ClassifiedTaxCategory/{*}Percent')
|
||||
if not tax_nodes:
|
||||
for elem in tree.findall('.//{*}TaxTotal'):
|
||||
tax_nodes += elem.findall('.//{*}TaxSubtotal/{*}Percent')
|
||||
|
||||
for tax_node in tax_nodes:
|
||||
tax = self.env['account.tax'].search([
|
||||
('company_id', '=', journal.company_id.id),
|
||||
('amount', '=', float(tax_node.text)),
|
||||
('amount_type', '=', 'percent'),
|
||||
('type_tax_use', '=', 'purchase'),
|
||||
], limit=1)
|
||||
if tax:
|
||||
taxes += tax
|
||||
else:
|
||||
logs.append(_("Could not retrieve the tax: %s %% for line '%s'.", float(tax_node.text), invoice_line.name))
|
||||
|
||||
invoice_line.tax_ids = taxes
|
||||
return logs
|
||||
return self._import_fill_invoice_line_taxes(journal, tax_nodes, invoice_line, inv_line_vals, logs)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# IMPORT : helpers
|
||||
|
||||
@@ -81,6 +81,7 @@ class TestUBLCommon(AccountEdiTestCommon):
|
||||
self.assert_same_invoice(invoice, new_invoice)
|
||||
|
||||
def _assert_imported_invoice_from_file(self, subfolder, filename, amount_total, amount_tax, list_line_subtotals,
|
||||
list_line_price_unit=None, list_line_discount=None, list_line_taxes=None,
|
||||
move_type='in_invoice', currency_id=None):
|
||||
"""
|
||||
Create an empty account.move, update the file to fill its fields, asserts the currency, total and tax amounts
|
||||
@@ -113,6 +114,16 @@ class TestUBLCommon(AccountEdiTestCommon):
|
||||
Counter(invoice.invoice_line_ids.mapped('price_subtotal')),
|
||||
Counter(list_line_subtotals),
|
||||
)
|
||||
if list_line_price_unit:
|
||||
self.assertEqual(invoice.invoice_line_ids.mapped('price_unit'), list_line_price_unit)
|
||||
if list_line_discount:
|
||||
# See test_import_tax_included: sometimes, it's impossible to retrieve the exact discount at import because
|
||||
# of rounding during export. The obtained discount might be 10.001 while the expected is 10.
|
||||
dp = self.env.ref('product.decimal_discount').precision_get("Discount")
|
||||
self.assertEqual([round(d, dp) for d in invoice.invoice_line_ids.mapped('discount')], list_line_discount)
|
||||
if list_line_taxes:
|
||||
for line, taxes in zip(invoice.invoice_line_ids, list_line_taxes):
|
||||
self.assertEqual(line.tax_ids, taxes)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# EXPORT HELPERS
|
||||
|
||||
-7
@@ -45,13 +45,6 @@
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>21.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeAllowanceCharge>
|
||||
<ram:ChargeIndicator>
|
||||
<udt:Indicator>false</udt:Indicator>
|
||||
</ram:ChargeIndicator>
|
||||
<ram:ActualAmount>99.00</ram:ActualAmount>
|
||||
<ram:ReasonCode>95</ram:ReasonCode>
|
||||
</ram:SpecifiedTradeAllowanceCharge>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>1782.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
<rsm:CrossIndustryInvoice xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>INV/2017/01/0001</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102">20170101</udt:DateTimeString>
|
||||
</ram:IssueDateTime>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>test narration</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
</rsm:ExchangedDocument>
|
||||
<rsm:SupplyChainTradeTransaction>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>1</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>product_a</ram:Name>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:GrossPriceProductTradePrice>
|
||||
<ram:ChargeAmount>95.24</ram:ChargeAmount>
|
||||
</ram:GrossPriceProductTradePrice>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>95.24</ram:ChargeAmount>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>5.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>95.24</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>2</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>product_a</ram:Name>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:GrossPriceProductTradePrice>
|
||||
<ram:ChargeAmount>100.00</ram:ChargeAmount>
|
||||
</ram:GrossPriceProductTradePrice>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>100.00</ram:ChargeAmount>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>5.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>100.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>3</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>product_a</ram:Name>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:GrossPriceProductTradePrice>
|
||||
<ram:ChargeAmount>190.48</ram:ChargeAmount>
|
||||
<ram:AppliedTradeAllowanceCharge>
|
||||
<ram:ChargeIndicator>
|
||||
<udt:Indicator>false</udt:Indicator>
|
||||
</ram:ChargeIndicator>
|
||||
<ram:ActualAmount>19.05</ram:ActualAmount>
|
||||
</ram:AppliedTradeAllowanceCharge>
|
||||
</ram:GrossPriceProductTradePrice>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>171.43</ram:ChargeAmount>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>5.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>171.43</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>4</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>product_a</ram:Name>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:GrossPriceProductTradePrice>
|
||||
<ram:ChargeAmount>200.00</ram:ChargeAmount>
|
||||
<ram:AppliedTradeAllowanceCharge>
|
||||
<ram:ChargeIndicator>
|
||||
<udt:Indicator>false</udt:Indicator>
|
||||
</ram:ChargeIndicator>
|
||||
<ram:ActualAmount>20.00</ram:ActualAmount>
|
||||
</ram:AppliedTradeAllowanceCharge>
|
||||
</ram:GrossPriceProductTradePrice>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>180.00</ram:ChargeAmount>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="C62">1.0</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>5.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>180.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:SellerTradeParty>
|
||||
<ram:Name>partner_1</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:PersonName>partner_1</ram:PersonName>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+1 (650) 555-0111</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID schemeID="SMTP">partner1@yourcompany.com</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>75000</ram:PostcodeCode>
|
||||
<ram:LineOne>Rue Jean Jaurès, 42</ram:LineOne>
|
||||
<ram:CityName>Paris</ram:CityName>
|
||||
<ram:CountryID>FR</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">FR05677404089</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:SellerTradeParty>
|
||||
<ram:BuyerTradeParty>
|
||||
<ram:Name>partner_2</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:PersonName>partner_2</ram:PersonName>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>52330</ram:PostcodeCode>
|
||||
<ram:LineOne>Rue Charles de Gaulle</ram:LineOne>
|
||||
<ram:CityName>Colombey-les-Deux-Églises</ram:CityName>
|
||||
<ram:CountryID>FR</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">FR35562153452</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:BuyerTradeParty>
|
||||
<ram:BuyerOrderReferencedDocument>
|
||||
<ram:IssuerAssignedID>INV/2017/01/0001</ram:IssuerAssignedID>
|
||||
</ram:BuyerOrderReferencedDocument>
|
||||
</ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:ApplicableHeaderTradeDelivery>
|
||||
<ram:ShipToTradeParty>
|
||||
<ram:Name>partner_2</ram:Name>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:PersonName>partner_2</ram:PersonName>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>52330</ram:PostcodeCode>
|
||||
<ram:LineOne>Rue Charles de Gaulle</ram:LineOne>
|
||||
<ram:CityName>Colombey-les-Deux-Églises</ram:CityName>
|
||||
<ram:CountryID>FR</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
</ram:ShipToTradeParty>
|
||||
<ram:ActualDeliverySupplyChainEvent>
|
||||
<ram:OccurrenceDateTime>
|
||||
<udt:DateTimeString format="102">20170101</udt:DateTimeString>
|
||||
</ram:OccurrenceDateTime>
|
||||
</ram:ActualDeliverySupplyChainEvent>
|
||||
</ram:ApplicableHeaderTradeDelivery>
|
||||
<ram:ApplicableHeaderTradeSettlement>
|
||||
<ram:PaymentReference>INV/2017/00001</ram:PaymentReference>
|
||||
<ram:InvoiceCurrencyCode>USD</ram:InvoiceCurrencyCode>
|
||||
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:TypeCode>42</ram:TypeCode>
|
||||
<ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:ProprietaryID>FR15001559627230</ram:ProprietaryID>
|
||||
</ram:PayeePartyCreditorFinancialAccount>
|
||||
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:CalculatedAmount>27.33</ram:CalculatedAmount>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:BasisAmount>546.67</ram:BasisAmount>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:DueDateTypeCode>5</ram:DueDateTypeCode>
|
||||
<ram:RateApplicablePercent>5.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradePaymentTerms>
|
||||
<ram:Description>30% Advance End of Following Month</ram:Description>
|
||||
<ram:DueDateDateTime>
|
||||
<udt:DateTimeString format="102">20170228</udt:DateTimeString>
|
||||
</ram:DueDateDateTime>
|
||||
</ram:SpecifiedTradePaymentTerms>
|
||||
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
<ram:LineTotalAmount>546.67</ram:LineTotalAmount>
|
||||
<ram:TaxBasisTotalAmount>546.67</ram:TaxBasisTotalAmount>
|
||||
<ram:TaxTotalAmount currencyID="USD">27.33</ram:TaxTotalAmount>
|
||||
<ram:GrandTotalAmount>574.00</ram:GrandTotalAmount>
|
||||
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
|
||||
<ram:DuePayableAmount>574.00</ram:DuePayableAmount>
|
||||
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
</ram:ApplicableHeaderTradeSettlement>
|
||||
</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>
|
||||
-7
@@ -45,13 +45,6 @@
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>21.0</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeAllowanceCharge>
|
||||
<ram:ChargeIndicator>
|
||||
<udt:Indicator>false</udt:Indicator>
|
||||
</ram:ChargeIndicator>
|
||||
<ram:ActualAmount>99.00</ram:ActualAmount>
|
||||
<ram:ReasonCode>95</ram:ReasonCode>
|
||||
</ram:SpecifiedTradeAllowanceCharge>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>1782.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
|
||||
@@ -69,6 +69,35 @@ class TestCIIFR(TestUBLCommon):
|
||||
'country_id': cls.env.ref('base.fr').id,
|
||||
})
|
||||
|
||||
cls.tax_5_purchase = cls.env['account.tax'].create({
|
||||
'name': 'tax_5',
|
||||
'amount_type': 'percent',
|
||||
'amount': 5,
|
||||
'type_tax_use': 'purchase',
|
||||
})
|
||||
|
||||
cls.tax_0_purchase = cls.env['account.tax'].create({
|
||||
'name': 'tax_0',
|
||||
'amount_type': 'percent',
|
||||
'amount': 0,
|
||||
'type_tax_use': 'purchase',
|
||||
})
|
||||
|
||||
cls.tax_5 = cls.env['account.tax'].create({
|
||||
'name': 'tax_5',
|
||||
'amount_type': 'percent',
|
||||
'amount': 5,
|
||||
'type_tax_use': 'sale',
|
||||
})
|
||||
|
||||
cls.tax_5_incl = cls.env['account.tax'].create({
|
||||
'name': 'tax_5_incl',
|
||||
'amount_type': 'percent',
|
||||
'amount': 5,
|
||||
'type_tax_use': 'sale',
|
||||
'price_include': True,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def setup_company_data(cls, company_name, chart_template):
|
||||
# OVERRIDE
|
||||
@@ -205,10 +234,99 @@ class TestCIIFR(TestUBLCommon):
|
||||
self.assertEqual(attachment.name, "factur-x.xml")
|
||||
self._assert_imported_invoice_from_etree(refund, attachment)
|
||||
|
||||
def test_export_tax_included(self):
|
||||
"""
|
||||
Tests whether the tax included price_units are correctly converted to tax excluded
|
||||
amounts in the exported xml
|
||||
"""
|
||||
invoice = self._generate_move(
|
||||
self.partner_1,
|
||||
self.partner_2,
|
||||
move_type='out_invoice',
|
||||
invoice_line_ids=[
|
||||
{
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [(6, 0, self.tax_5_incl.ids)],
|
||||
},
|
||||
{
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [(6, 0, self.tax_5.ids)],
|
||||
},
|
||||
{
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 200,
|
||||
'discount': 10,
|
||||
'tax_ids': [(6, 0, self.tax_5_incl.ids)],
|
||||
},
|
||||
{
|
||||
'product_id': self.product_a.id,
|
||||
'quantity': 1,
|
||||
'price_unit': 200,
|
||||
'discount': 10,
|
||||
'tax_ids': [(6, 0, self.tax_5.ids)],
|
||||
},
|
||||
],
|
||||
)
|
||||
self._assert_invoice_attachment(
|
||||
invoice,
|
||||
xpaths='''
|
||||
<xpath expr="./*[local-name()='ExchangedDocument']/*[local-name()='ID']" position="replace">
|
||||
<ID>___ignore___</ID>
|
||||
</xpath>
|
||||
<xpath expr=".//*[local-name()='IssuerAssignedID']" position="replace">
|
||||
<IssuerAssignedID>___ignore___</IssuerAssignedID>
|
||||
</xpath>
|
||||
''',
|
||||
expected_file='from_odoo/facturx_out_invoice_tax_incl.xml'
|
||||
)
|
||||
|
||||
####################################################
|
||||
# Test import
|
||||
####################################################
|
||||
|
||||
def test_import_tax_included(self):
|
||||
"""
|
||||
Tests whether the tax included / tax excluded are correctly decoded when
|
||||
importing a document. The imported xml represents the following invoice:
|
||||
|
||||
Description Quantity Unit Price Disc (%) Taxes Amount
|
||||
--------------------------------------------------------------------------------
|
||||
Product A 1 100 0 5% (incl) 95.24
|
||||
Product A 1 100 0 5% (not incl) 100
|
||||
Product A 2 200 10 5% (incl) 171.43
|
||||
Product A 2 200 10 5% (not incl) 180
|
||||
-----------------------
|
||||
Untaxed Amount: 546.67
|
||||
Taxes: 27.334
|
||||
-----------------------
|
||||
Total: 574.004
|
||||
"""
|
||||
self._assert_imported_invoice_from_file(
|
||||
subfolder='tests/test_files/from_odoo',
|
||||
filename='facturx_out_invoice_tax_incl.xml',
|
||||
amount_total=574.004,
|
||||
amount_tax=27.334,
|
||||
list_line_subtotals=[95.24, 100, 171.43, 180],
|
||||
# /!\ The price_unit are different for taxes with price_include, because all amounts in Factur-X should be
|
||||
# tax excluded. At import, the tax included amounts are thus converted into tax excluded ones.
|
||||
# Yet, the line subtotals and total will be the same (if an equivalent tax exist with price_include = False)
|
||||
list_line_price_unit=[95.24, 100, 190.48, 200],
|
||||
# rounding error since for line 3: we round several times...
|
||||
# when exporting the invoice, we compute the price tax excluded = 200/1.05 ~= 190.48
|
||||
# then, when computing the discount amount: 190.48 * 0.1 ~= 19.05 => price net amount = 171.43
|
||||
# Thus, at import: price_unit = 190.48, and discount = 100 * (1 - 171.43 / 190.48) = 10.001049979
|
||||
list_line_discount=[0, 0, 10, 10],
|
||||
# Again, all taxes in the imported invoice are price_include = False
|
||||
list_line_taxes=[self.tax_5_purchase]*4,
|
||||
move_type='in_invoice',
|
||||
currency_id=self.env['res.currency'].search([('name', '=', 'USD')], limit=1).id,
|
||||
)
|
||||
|
||||
def test_import_fnfe_examples(self):
|
||||
# Source: official documentation of the FNFE (subdirectory: "5. FACTUR-X 1.0.06 - Examples")
|
||||
subfolder = 'tests/test_files/from_factur-x_doc'
|
||||
|
||||
@@ -23,6 +23,12 @@ class TestCIIUS(TestUBLCommon):
|
||||
'country_id': cls.env.ref('base.us').id,
|
||||
})
|
||||
|
||||
cls.tax_0 = cls.env['account.tax'].create({
|
||||
'name': "Tax 0%",
|
||||
'type_tax_use': 'purchase',
|
||||
'amount': 0,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def setup_company_data(cls, company_name, chart_template):
|
||||
# OVERRIDE
|
||||
|
||||
Reference in New Issue
Block a user