From c8146daf6cd7b63d9e3726b64be12149ecf2d87b Mon Sep 17 00:00:00 2001 From: Ugaitz Olaizola Date: Thu, 1 Feb 2024 15:18:45 +0100 Subject: [PATCH] =?UTF-8?q?[IMP]=20l10n=5Fes=5Fedi=5Ftbai,=20support=20out?= =?UTF-8?q?=20invoices=20with=20taxes=20of=20type=20"retenci=C3=B3n":?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do not take into account "retención" type taxes in the sum of the total price of the invoice lines, these taxes are of retention types and are declared in RetencionSoportada XML node. Add amount_retention in invoice values and change template_invoice_factura to activate RetencionSoportada xml node. Before this commit: Invoices with "retención" type taxes are not declared correctly, validation errors in the response of the tax agency. With this commit: Invoices with "retención" type taxes are declared correctly and accepted with no validation errors in the response of the tax agency. closes odoo/odoo#152978 X-original-commit: ff961775d7f175c4f71f33f9b3955c04a234db55 Signed-off-by: Josse Colpaert --- .../data/template_invoice.xml | 2 +- .../models/account_edi_format.py | 36 +++++++++++++++---- addons/l10n_es_edi_tbai/tests/test_edi_xml.py | 14 ++++++++ doc/cla/individual/uolaizola.md | 11 ++++++ 4 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 doc/cla/individual/uolaizola.md diff --git a/addons/l10n_es_edi_tbai/data/template_invoice.xml b/addons/l10n_es_edi_tbai/data/template_invoice.xml index 4067f490c40..edfeeadb419 100644 --- a/addons/l10n_es_edi_tbai/data/template_invoice.xml +++ b/addons/l10n_es_edi_tbai/data/template_invoice.xml @@ -119,7 +119,7 @@ - + diff --git a/addons/l10n_es_edi_tbai/models/account_edi_format.py b/addons/l10n_es_edi_tbai/models/account_edi_format.py index 32d4d2c67cd..ef1ddbae073 100644 --- a/addons/l10n_es_edi_tbai/models/account_edi_format.py +++ b/addons/l10n_es_edi_tbai/models/account_edi_format.py @@ -339,11 +339,12 @@ class AccountEdiFormat(models.Model): else: balance_before_discount = line.balance / (1 - line.discount / 100) discount = (balance_before_discount - line.balance) + line_price_total = self._l10n_es_tbai_get_invoice_line_price_total(line) if not any([t.l10n_es_type == 'sujeto_isp' for t in line.tax_ids]): - total = line.price_total * abs(line.balance / line.amount_currency if line.amount_currency != 0 else 1) * -refund_sign + total = line_price_total * abs(line.balance / line.amount_currency if line.amount_currency != 0 else 1) * -refund_sign else: - total = abs(line.balance) * -refund_sign * (-1 if line.price_total < 0 else 1) + total = abs(line.balance) * -refund_sign * (-1 if line_price_total < 0 else 1) invoice_lines.append({ 'line': line, 'discount': discount * refund_sign, @@ -353,9 +354,10 @@ class AccountEdiFormat(models.Model): }) values['invoice_lines'] = invoice_lines # Tax details (desglose) - importe_total, desglose = self._l10n_es_tbai_get_importe_desglose(invoice) + importe_total, desglose, amount_retention = self._l10n_es_tbai_get_importe_desglose(invoice) values['amount_total'] = importe_total values['invoice_info'] = desglose + values['amount_retention'] = amount_retention * refund_sign if amount_retention != 0.0 else 0.0 # Regime codes (ClaveRegimenEspecialOTrascendencia) # NOTE there's 11 more codes to implement, also there can be up to 3 in total @@ -368,18 +370,35 @@ class AccountEdiFormat(models.Model): return values + def _l10n_es_tbai_get_invoice_line_price_total(self, invoice_line): + price_total = invoice_line.price_total + retention_tax_lines = invoice_line.tax_ids.filtered(lambda t: t.l10n_es_type == "retencion") + if retention_tax_lines: + line_discount_price_unit = invoice_line.price_unit * (1 - (invoice_line.discount / 100.0)) + tax_lines_no_retention = invoice_line.tax_ids - retention_tax_lines + if tax_lines_no_retention: + taxes_res = tax_lines_no_retention.compute_all(line_discount_price_unit, + quantity=invoice_line.quantity, + currency=invoice_line.currency_id, + product=invoice_line.product_id, + partner=invoice_line.move_id.partner_id, + is_refund=invoice_line.is_refund) + price_total = taxes_res['total_included'] + return price_total + def _l10n_es_tbai_get_importe_desglose(self, invoice): com_partner = invoice.commercial_partner_id sign = -1 if invoice.move_type in ('out_refund', 'in_refund') else 1 if com_partner.country_id.code in ('ES', False) and not (com_partner.vat or '').startswith("ESN"): tax_details_info_vals = self._l10n_es_edi_get_invoices_tax_details_info(invoice) + tax_amount_retention = tax_details_info_vals['tax_amount_retention'] desglose = {'DesgloseFactura': tax_details_info_vals['tax_details_info']} desglose['DesgloseFactura'].update({'S1': tax_details_info_vals['S1_list'], 'S2': tax_details_info_vals['S2_list']}) importe_total = round(sign * ( tax_details_info_vals['tax_details']['base_amount'] + tax_details_info_vals['tax_details']['tax_amount'] - - tax_details_info_vals['tax_amount_retention'] + - tax_amount_retention ), 2) else: tax_details_info_service_vals = self._l10n_es_edi_get_invoices_tax_details_info( @@ -390,6 +409,8 @@ class AccountEdiFormat(models.Model): invoice, filter_invl_to_apply=lambda x: any(t.tax_scope == 'consu' for t in x.tax_ids) ) + service_retention = tax_details_info_service_vals['tax_amount_retention'] + consu_retention = tax_details_info_consu_vals['tax_amount_retention'] desglose = {} if tax_details_info_service_vals['tax_details_info']: desglose.setdefault('DesgloseTipoOperacion', {}) @@ -407,12 +428,13 @@ class AccountEdiFormat(models.Model): importe_total = round(sign * ( tax_details_info_service_vals['tax_details']['base_amount'] + tax_details_info_service_vals['tax_details']['tax_amount'] - - tax_details_info_service_vals['tax_amount_retention'] + - service_retention + tax_details_info_consu_vals['tax_details']['base_amount'] + tax_details_info_consu_vals['tax_details']['tax_amount'] - - tax_details_info_consu_vals['tax_amount_retention'] + - consu_retention ), 2) - return importe_total, desglose + tax_amount_retention = service_retention + consu_retention + return importe_total, desglose, tax_amount_retention def _l10n_es_tbai_get_trail_values(self, invoice, cancel): prev_invoice = invoice.company_id._get_l10n_es_tbai_last_posted_invoice(invoice) diff --git a/addons/l10n_es_edi_tbai/tests/test_edi_xml.py b/addons/l10n_es_edi_tbai/tests/test_edi_xml.py index 8050a1a10a5..3930c8cd620 100644 --- a/addons/l10n_es_edi_tbai/tests/test_edi_xml.py +++ b/addons/l10n_es_edi_tbai/tests/test_edi_xml.py @@ -118,6 +118,20 @@ class TestEdiTbaiXmls(TestEsEdiTbaiCommon): xml_expected = self.with_applied_xpath(xml_expected_base, xpath) self.assertXmlTreeEqual(xml_doc, xml_expected) + def test_xml_tree_post_retention(self): + self.out_invoice.invoice_line_ids.tax_ids = [(4, self._get_tax_by_xml_id('s_irpf15').id)] + with freeze_time(self.frozen_today): + xml_doc = self.edi_format._get_l10n_es_tbai_invoice_xml(self.out_invoice, cancel=False)[self.out_invoice]['xml_file'] + xml_doc.remove(xml_doc.find("Signature", namespaces=NS_MAP)) + xml_expected_base = etree.fromstring(super().L10N_ES_TBAI_SAMPLE_XML_POST) + xpath = """ + + 600.00 + + """ + xml_expected = self.with_applied_xpath(xml_expected_base, xpath) + self.assertXmlTreeEqual(xml_doc, xml_expected) + def test_xml_tree_in_post(self): """Test XML of vendor bill for LROE Batuz""" with freeze_time(self.frozen_today): diff --git a/doc/cla/individual/uolaizola.md b/doc/cla/individual/uolaizola.md new file mode 100644 index 00000000000..faf19b2779d --- /dev/null +++ b/doc/cla/individual/uolaizola.md @@ -0,0 +1,11 @@ +Spain, 2024-02-01 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Ugaitz Olaizola Arbelaitz uolaizola@binovo.es https://github.com/uolaizola