[IMP] l10n_es_edi_tbai, support out invoices with taxes of type "retención":

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 <jco@odoo.com>
This commit is contained in:
Ugaitz Olaizola
2024-02-07 18:34:22 +00:00
parent c9a8c9ab6b
commit c8146daf6c
4 changed files with 55 additions and 8 deletions
@@ -119,7 +119,7 @@
</IDDetalleFactura>
</DetallesFactura>
<ImporteTotalFactura t-out="format_float(amount_total)"/>
<!-- <RetencionSoportada/> NOTE (potentially has to be computed/decided manually) -->
<RetencionSoportada t-if="amount_retention != 0.0" t-out="format_float(amount_retention)"/>
<!-- <BaseImponibleACoste/> NOTE (only applicable with ClaveRegimenIvaOpTrascendencia 06, not supported yet) -->
<Claves>
<IDClave t-foreach="regime_key" t-as="key">
@@ -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)
@@ -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 = """
<xpath expr="//ImporteTotalFactura" position="after">
<RetencionSoportada>600.00</RetencionSoportada>
</xpath>
"""
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):
+11
View File
@@ -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