[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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user