diff --git a/addons/account_edi/models/__init__.py b/addons/account_edi/models/__init__.py
index 1d924dbdbb4..460df054a3f 100644
--- a/addons/account_edi/models/__init__.py
+++ b/addons/account_edi/models/__init__.py
@@ -8,3 +8,4 @@ from . import account_payment
from . import ir_actions_report
from . import mail_template
from . import ir_attachment
+from . import uom
diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py
index d23448b28e7..3799faa645c 100644
--- a/addons/account_edi/models/account_move.py
+++ b/addons/account_edi/models/account_move.py
@@ -159,6 +159,7 @@ class AccountMove(models.Model):
tax_details['base_amount_currency'] += tax_values['base_amount_currency']
tax_details['tax_amount'] += tax_values['tax_amount']
tax_details['tax_amount_currency'] += tax_values['tax_amount_currency']
+ tax_details['exemption_reason'] = tax_values['tax_id'].name
tax_details['group_tax_details'].append(tax_values)
def _prepare_edi_tax_details(self, filter_to_apply=None, filter_invl_to_apply=None, grouping_key_generator=None, compute_mode='tax_details'):
@@ -662,7 +663,9 @@ class AccountMoveLine(models.Model):
'price_subtotal_unit': self.currency_id.round(self.price_subtotal / self.quantity) if self.quantity else 0.0,
'price_total_unit': self.currency_id.round(self.price_total / self.quantity) if self.quantity else 0.0,
'price_discount': gross_price_subtotal - self.price_subtotal,
+ 'price_discount_unit': (gross_price_subtotal - self.price_subtotal) / self.quantity if self.quantity else 0.0,
'gross_price_total_unit': self.currency_id.round(gross_price_subtotal / self.quantity) if self.quantity else 0.0,
+ 'unece_uom_code': self.product_id.product_tmpl_id.uom_id._get_unece_code(),
}
return res
diff --git a/addons/account_edi/models/uom.py b/addons/account_edi/models/uom.py
new file mode 100644
index 00000000000..0b8f54c030c
--- /dev/null
+++ b/addons/account_edi/models/uom.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models
+
+
+class UoM(models.Model):
+ _inherit = 'uom.uom'
+
+ def _get_unece_code(self):
+ """ Returns the UNECE code used for international trading for corresponding to the UoM as per
+ https://unece.org/fileadmin/DAM/cefact/recommendations/bkup_htm/add2d.htm.
+ """
+ if len(self) != 1:
+ return 'C62'
+
+ xml_id = self.env['ir.model.data'].sudo().search([
+ ('model', '=', 'uom.uom'),
+ ('res_id', '=', self.id),
+ ]).name
+ mapping = {
+ 'product_uom_unit': 'C62',
+ 'product_uom_dozen': 'DZN',
+ 'product_uom_kgm': 'KGM',
+ 'product_uom_gram': 'GRM',
+ 'product_uom_day': 'DAY',
+ 'product_uom_hour': 'HUR',
+ 'product_uom_ton': 'TNE',
+ 'product_uom_meter': 'MTR',
+ 'product_uom_km': 'KTM',
+ 'product_uom_cm': 'CMT',
+ 'product_uom_litre': 'LTR',
+ 'product_uom_lb': 'LBR',
+ 'product_uom_oz': 'ONZ',
+ 'product_uom_inch': 'INH',
+ 'product_uom_foot': 'FOT',
+ 'product_uom_mile': 'SMI',
+ 'product_uom_floz': 'OZA',
+ 'product_uom_qt': 'QT',
+ 'product_uom_gal': 'GLL',
+ 'product_uom_cubic_meter': 'MTQ',
+ 'product_uom_cubic_inch': 'INQ',
+ 'product_uom_cubic_foot': 'FTQ',
+ }
+ return mapping.get(xml_id, 'C62')
diff --git a/addons/account_edi_facturx/data/account_edi_data.xml b/addons/account_edi_facturx/data/account_edi_data.xml
index d581c468a5c..4e8b48fa19f 100644
--- a/addons/account_edi_facturx/data/account_edi_data.xml
+++ b/addons/account_edi_facturx/data/account_edi_data.xml
@@ -8,4 +8,10 @@
-
\ No newline at end of file
+
+
+ edi.use_pdfa
+ false
+
+
+
diff --git a/addons/account_edi_facturx/data/facturx_templates.xml b/addons/account_edi_facturx/data/facturx_templates.xml
index 106df718a1b..c39fa53bdd5 100644
--- a/addons/account_edi_facturx/data/facturx_templates.xml
+++ b/addons/account_edi_facturx/data/facturx_templates.xml
@@ -29,50 +29,40 @@
-
+ false
-
+
-
-
-
-
-
-
+
-
+
-
-
-
+
+ VAT
+
+
@@ -87,15 +77,10 @@
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
-
-
-
-
-
-
-
-
-
+
+
+
+
@@ -149,6 +134,7 @@
+
@@ -162,6 +148,7 @@
+
@@ -172,25 +159,27 @@
-
+
-
+
-
+
+
+
+ 42
@@ -198,20 +187,28 @@
-
+ VAT
+
+
+
+
+ t-if="tax_detail_vals['amount_type'] == 'percent'"
+ t-esc="tax_detail_vals['amount']"/>
+
+
+
+
+
+
@@ -223,22 +220,17 @@
diff --git a/addons/account_edi_facturx/models/__init__.py b/addons/account_edi_facturx/models/__init__.py
index af22260edd0..3ea895ec6b2 100644
--- a/addons/account_edi_facturx/models/__init__.py
+++ b/addons/account_edi_facturx/models/__init__.py
@@ -1,4 +1,5 @@
# -*- encoding: utf-8 -*-
from . import account_edi_format
+from . import account_tax
from . import mail_template
diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py
index 228fd1f5fcc..77267fca4af 100644
--- a/addons/account_edi_facturx/models/account_edi_format.py
+++ b/addons/account_edi_facturx/models/account_edi_format.py
@@ -36,7 +36,7 @@ class AccountEdiFormat(models.Model):
if not edi_document.attachment_id:
return
- pdf_writer.embed_odoo_attachment(edi_document.attachment_id, subtype='application/xml')
+ pdf_writer.embed_odoo_attachment(edi_document.attachment_id, subtype='text/xml')
if not pdf_writer.is_pdfa and str2bool(self.env['ir.config_parameter'].sudo().get_param('edi.use_pdfa', 'False')):
try:
pdf_writer.convert_to_pdfa()
@@ -59,16 +59,29 @@ class AccountEdiFormat(models.Model):
def format_monetary(number, currency):
# Format the monetary values to avoid trailing decimals (e.g. 90.85000000000001).
+ if currency.is_zero(number): # Ensure that we never return -0.0
+ number = 0.0
return float_repr(number, currency.decimal_places)
self.ensure_one()
# Create file content.
+ seller_siret = 'siret' in invoice.company_id._fields and invoice.company_id.siret or invoice.company_id.company_registry
+ buyer_siret = 'siret' in invoice.commercial_partner_id._fields and invoice.commercial_partner_id.siret
+ tax_detail_vals = invoice._prepare_edi_tax_details(
+ grouping_key_generator=lambda tax_values: {
+ 'unece_tax_category_code': tax_values['tax_id']._get_unece_category_code(invoice.commercial_partner_id, invoice.company_id),
+ 'amount': tax_values['tax_id'].amount,
+ 'amount_type': tax_values['tax_id'].amount_type, # We need to check the type
+ }
+ )
template_values = {
**invoice._prepare_edi_vals_to_export(),
- 'tax_details': invoice._prepare_edi_tax_details(),
+ 'tax_details': tax_detail_vals,
'format_date': format_date,
'format_monetary': format_monetary,
'is_html_empty': is_html_empty,
+ 'seller_specified_legal_organization': seller_siret,
+ 'buyer_specified_legal_organization': buyer_siret,
}
xml_content = markupsafe.Markup("")
@@ -172,17 +185,22 @@ class AccountEdiFormat(models.Model):
if elements:
invoice_form.narration = elements[0].text
- # Total amount.
- elements = tree.xpath('//ram:GrandTotalAmount', namespaces=tree.nsmap)
+ # Get currency string for new invoices, or invoices coming from outside
+ elements = tree.xpath('//ram:InvoiceCurrencyCode', namespaces=tree.nsmap)
if elements:
+ currency_str = elements[0].text
+ # Fallback for old invoices from odoo where the InvoiceCurrencyCode was not present
+ else:
+ elements = tree.xpath('//ram:TaxTotalAmount', namespaces=tree.nsmap)
+ if elements:
+ currency_str = elements[0].attrib.get('currencyID', None)
- # Currency.
- currency_str = elements[0].attrib.get('currencyID', None)
- if currency_str:
- invoice_form.currency_id = self._retrieve_currency(currency_str)
+ # Currency.
+ if currency_str:
+ invoice_form.currency_id = self._retrieve_currency(currency_str)
- # Store xml total amount.
- amount_total_import = total_amount * refund_sign
+ # Store xml total amount.
+ amount_total_import = total_amount * refund_sign
# Date.
elements = tree.xpath('//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString', namespaces=tree.nsmap)
diff --git a/addons/account_edi_facturx/models/account_tax.py b/addons/account_edi_facturx/models/account_tax.py
new file mode 100644
index 00000000000..feb8b8543a4
--- /dev/null
+++ b/addons/account_edi_facturx/models/account_tax.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models
+
+
+class AccountTax(models.Model):
+ _inherit = 'account.tax'
+
+ def _get_unece_category_code(self, customer, supplier):
+ """ By default, this method will try to compute the tax category (used by EDI for example) based on the amount
+ and the tax repartition lines. This is hack-ish~ but a valid solution to get a default value in stable.
+ See https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred5305.htm for the codes.
+ """
+ self.ensure_one()
+ # Defaulting to standard tax.
+ category = 'S'
+ if self.type_tax_use == 'sale':
+ eu_countries = self.env.ref('base.europe').country_ids
+ if supplier.country_id in eu_countries and customer.country_id not in eu_countries:
+ category = 'G'
+ else:
+ if customer.country_id != supplier.country_id \
+ and customer.country_id in eu_countries \
+ and supplier.country_id in eu_countries:
+ category = 'K'
+ # Taxes with a Zero amount will get the E code. (Exempt)
+ elif self.amount == 0:
+ category = 'E'
+
+ return category
diff --git a/addons/account_edi_facturx/tests/test_facturx.py b/addons/account_edi_facturx/tests/test_facturx.py
index 454a3871d28..f9256c671f0 100644
--- a/addons/account_edi_facturx/tests/test_facturx.py
+++ b/addons/account_edi_facturx/tests/test_facturx.py
@@ -82,76 +82,77 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
- 275.000
+ 275.000
- true
+ false
- 20.0
+ 55.000
- 220.000
+ 220.000
- 5.0
+ 5.0
+ VAT
+ S
20.0
- 1100.000
+ 1100.000
company_1_data
-
- company_1_data
-
partner_b
-
- partner_b
-
- INV/2017/00001: INV/2017/00001
+ INV/2017/00001
partner_b
-
- partner_b
-
+ Gol
- 220.000
- 1100.000
+ 220.000
+ VAT
+ 1100.000
+ S
20.0
+
+
+ 20170101
+
+
20170101
- 1100.000
- 1100.000
+ 1100.000
+ 1100.000
220.000
- 1320.000
- 0.000
- 1320.000
+ 1320.000
+ 0.000
+ 1320.000
@@ -174,49 +175,66 @@ class TestAccountEdiFacturx(AccountEdiTestCommon):
})
applied_xpath = '''
-
- 200.000
-
-
-
-
- 10.0
-
-
- 20.0
-
-
- 1000.000
-
-
-
-
-
-
- 100.000
- 1000.000
- 10.0
-
-
- 220.000
- 1100.000
- 20.0
-
-
-
- 20170101
-
-
-
- 1000.000
- 1000.000
- 320.000
- 1320.000
- 0.000
- 1320.000
-
-
-
+
+ 50.000
+
+
+ 200.000
+
+
+
+
+ VAT
+ S
+ 10.0
+
+
+ VAT
+ S
+ 20.0
+
+
+ 1000.000
+
+
+
+
+
+ Gol
+
+ 100.000
+ VAT
+ 1000.000
+ S
+ 10.0
+
+
+ 220.000
+ VAT
+ 1100.000
+ S
+ 20.0
+
+
+
+ 20170101
+
+
+
+
+ 20170101
+
+
+
+ 1000.000
+ 1000.000
+ 320.000
+ 1320.000
+ 0.000
+ 1320.000
+
+
+
'''
self.assert_generated_file_equal(self.invoice, self.expected_invoice_facturx_values, applied_xpath)