From 7f93f72a4f874e7073c0fec8ab0a89ebbd13e029 Mon Sep 17 00:00:00 2001
From: Julien Van Roy <juvr@odoo.com>
Date: Fri, 25 Nov 2022 09:03:07 +0000
Subject: [PATCH] [FIX] {l10n_}account_edi_ubl_cii{_tests}: handle
 price_include taxes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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#106563

X-original-commit: 9a5ba15cb308a55cb9bf6f885b72c12eaf647170
Signed-off-by: William André (wan) <wan@odoo.com>
Signed-off-by: Julien Van Roy <juvr@odoo.com>
---
 .../data/cii_22_templates.xml                 |  18 +-
 .../models/account_edi_common.py              |  65 ++++-
 .../models/account_edi_xml_cii_facturx.py     |  26 +-
 .../models/account_edi_xml_ubl_20.py          |  25 +-
 .../tests/common.py                           |  11 +
 .../from_odoo/facturx_out_invoice.xml         |   7 -
 .../facturx_out_invoice_tax_incl.xml          | 239 ++++++++++++++++++
 .../from_odoo/facturx_out_refund.xml          |   7 -
 .../tests/test_xml_cii_fr.py                  | 118 +++++++++
 .../tests/test_xml_cii_us.py                  |   6 +
 10 files changed, 439 insertions(+), 83 deletions(-)
 create mode 100644 addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml

diff --git a/addons/account_edi_ubl_cii/data/cii_22_templates.xml b/addons/account_edi_ubl_cii/data/cii_22_templates.xml
index 768dab9c8420..2bc3c38a11cc 100644
--- a/addons/account_edi_ubl_cii/data/cii_22_templates.xml
+++ b/addons/account_edi_ubl_cii/data/cii_22_templates.xml
@@ -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)"/>
diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py
index 0b304296e0c6..46e49f1283a4 100644
--- a/addons/account_edi_ubl_cii/models/account_edi_common.py
+++ b/addons/account_edi_ubl_cii/models/account_edi_common.py
@@ -416,7 +416,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)
@@ -458,6 +458,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
@@ -479,10 +485,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
@@ -527,9 +532,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:
@@ -538,17 +541,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 !
diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py b/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py
index 99cb536eb087..7544ba5a1530 100644
--- a/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py
+++ b/addons/account_edi_ubl_cii/models/account_edi_xml_cii_facturx.py
@@ -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', '=', journal.type),
-            ], 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
diff --git a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py
index e4e7bb7c1bec..cb227c7860b7 100644
--- a/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py
+++ b/addons/account_edi_ubl_cii/models/account_edi_xml_ubl_20.py
@@ -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', '=', journal.type),
-            ], 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
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/common.py b/addons/l10n_account_edi_ubl_cii_tests/tests/common.py
index ec7d36be2eca..79b50aaff32d 100644
--- a/addons/l10n_account_edi_ubl_cii_tests/tests/common.py
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/common.py
@@ -82,6 +82,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
@@ -114,6 +115,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
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml
index 8c7f1a242ba6..7fa151c8f3e6 100644
--- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice.xml
@@ -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>
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml
new file mode 100644
index 000000000000..ae2eef40989f
--- /dev/null
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_invoice_tax_incl.xml
@@ -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&#232;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-&#201;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-&#201;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>
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml
index d1ff9a334700..1c4ed46bcbd0 100644
--- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_files/from_odoo/facturx_out_refund.xml
@@ -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>
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py
index f7211cf84d4e..2795414946cc 100644
--- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_fr.py
@@ -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'
diff --git a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py
index c90be3b22e4f..4eb6b1db1ba6 100644
--- a/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py
+++ b/addons/l10n_account_edi_ubl_cii_tests/tests/test_xml_cii_us.py
@@ -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
-- 
GitLab