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)