diff --git a/l10n_es_aeat_sii_oca/models/account_move.py b/l10n_es_aeat_sii_oca/models/account_move.py index 76ad71e02b8..2a44d7b598a 100644 --- a/l10n_es_aeat_sii_oca/models/account_move.py +++ b/l10n_es_aeat_sii_oca/models/account_move.py @@ -212,6 +212,7 @@ def _get_sii_tax_dict(self, tax_line, tax_lines): if req_tax: tax_dict["TipoRecargoEquivalencia"] = req_tax.amount tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"] + tax_dict = self._clean_sii_tax_dict(tax, self.move_type, tax_dict) return tax_dict def _get_document_amount_total(self): diff --git a/l10n_es_aeat_sii_oca/models/sii_mixin.py b/l10n_es_aeat_sii_oca/models/sii_mixin.py index 2700d92bdc0..982b33798bf 100644 --- a/l10n_es_aeat_sii_oca/models/sii_mixin.py +++ b/l10n_es_aeat_sii_oca/models/sii_mixin.py @@ -10,7 +10,7 @@ from odoo import _, api, exceptions, fields, models from odoo.exceptions import UserError, ValidationError from odoo.modules.registry import Registry -from odoo.tools.float_utils import float_compare +from odoo.tools.float_utils import float_compare, float_is_zero from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys @@ -422,7 +422,8 @@ def _get_sii_tax_dict(self, tax_line, tax_lines): else: tax_type = abs(tax.amount) tax_dict = {"TipoImpositivo": str(tax_type), "BaseImponible": tax_base_amount} - if self._get_mapping_key() in ["out_invoice", "out_refund"]: + mapping_key = self._get_mapping_key() + if mapping_key in ["out_invoice", "out_refund"]: key = "CuotaRepercutida" else: key = "CuotaSoportada" @@ -432,6 +433,7 @@ def _get_sii_tax_dict(self, tax_line, tax_lines): if req_tax: tax_dict["TipoRecargoEquivalencia"] = req_tax.amount tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"] + tax_dict = self._clean_sii_tax_dict(tax, mapping_key, tax_dict) return tax_dict def _get_no_taxable_cause(self): @@ -741,6 +743,40 @@ def _get_aeat_invoice_dict(self): ) return inv_dict + def _clean_sii_tax_dict(self, tax, move_type, tax_dict): + """ + Clean tax dict for corner cases that produces SII rejections. + :param tax: tax record + :param move_type: invoice move type + :param tax_dict: tax dict to clean + :return: cleaned tax dict + + Corner case 1: + - document with amount_total 0.0 + - amount_untaxed -0.1 and amount_tax 0.1 or + 0.1 amount_untaxed and -0.1 amount_tax due to rounding issues. + (example in test_get_invoice_data_tax_price_included_corner_case) + It should be sent with 0.0 values to avoid SII rejection with error: + 1231:'El campo CuotaRepercutida y BaseImponible deben tener el mismo signo.' + """ + if tax.price_include: + base_amount = round(tax_dict.get("BaseImponible", 0.0), 2) + key = ( + "CuotaRepercutida" + if move_type in ["out_invoice", "out_refund"] + else "CuotaSoportada" + ) + tax_amount = round(tax_dict.get(key, 0.0), 2) + + if ( + float_compare(abs(base_amount), 0.01, precision_digits=2) == 0 + and float_compare(abs(tax_amount), 0.01, precision_digits=2) == 0 + and float_is_zero(base_amount + tax_amount, precision_digits=2) + ): + tax_dict[key] = 0.0 + tax_dict["BaseImponible"] = 0.0 + return tax_dict + def _get_account_registration_date(self): """Hook method to allow the setting of the account registration date of each supplier invoice. The SII recommends to set the send date as diff --git a/l10n_es_aeat_sii_oca/tests/json/sii_out_invoice_s_iva21s_s_iva21s_s_iva21s_s_iva21s_dict.json b/l10n_es_aeat_sii_oca/tests/json/sii_out_invoice_s_iva21s_s_iva21s_s_iva21s_s_iva21s_dict.json new file mode 100644 index 00000000000..d6d61440014 --- /dev/null +++ b/l10n_es_aeat_sii_oca/tests/json/sii_out_invoice_s_iva21s_s_iva21s_s_iva21s_s_iva21s_dict.json @@ -0,0 +1,35 @@ +{ + "IDFactura": { + "IDEmisorFactura": {"NIF": "U2687761C"}, + "NumSerieFacturaEmisor": "TEST001", + "FechaExpedicionFacturaEmisor": "01-01-2020" + }, + "PeriodoLiquidacion": {"Ejercicio": 2020, "Periodo": "01"}, + "FacturaExpedida": { + "TipoFactura": "F1", + "ClaveRegimenEspecialOTrascendencia": "01", + "DescripcionOperacion": "/", + "TipoDesglose": { + "DesgloseTipoOperacion": { + "PrestacionServicios": { + "Sujeta": { + "NoExenta": { + "TipoNoExenta": "S1", + "DesgloseIVA": { + "DetalleIVA": [ + { + "TipoImpositivo": "21.0", + "BaseImponible": 0.0, + "CuotaRepercutida": 0.0 + } + ] + } + } + } + } + } + }, + "ImporteTotal": 0.0, + "Contraparte": {"NombreRazon": "Test partner", "NIF": "F35999705"} + } +} diff --git a/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py b/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py index 6fb03874311..3745777b4a1 100644 --- a/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py +++ b/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py @@ -545,3 +545,35 @@ def test_start_date(self): self.company.sii_start_date = False self.assertTrue(invoice2.sii_enabled) self.assertTrue(invoice2.filtered_domain([("sii_enabled", "=", True)])) + + def test_get_invoice_data_tax_price_included_corner_case(self): + xml_id = "l10n_es.{}_account_tax_template_{}".format( + self.company.id, "s_iva21s" + ) + tax = self.env.ref(xml_id) + tax.price_include = True + mapping = [ + ( + "out_invoice", + [ + (-45, ["s_iva21s"]), + (15, ["s_iva21s"]), + (15, ["s_iva21s"]), + (15, ["s_iva21s"]), + ], + {}, + ), + ( + "out_invoice", + [ + (45, ["s_iva21s"]), + (-15, ["s_iva21s"]), + (-15, ["s_iva21s"]), + (-15, ["s_iva21s"]), + ], + {}, + ), + ] + for inv_type, lines, extra_vals in mapping: + self._create_and_test_invoice_sii_dict(inv_type, lines, extra_vals) + return