|
| 1 | +""" |
| 2 | +Tests para map_tax en posiciones fiscales argentinas (l10n_ar_tax). |
| 3 | +Escenarios cubiertos: |
| 4 | + 1. Pos. Fiscal sólo percepción con domestic FP con sustitución → IVA 21% no se reemplaza por IVA 0% |
| 5 | + 2. Pos. Fiscal con tax_ids explícitos → map_tax aplica la sustitución correctamente (super) |
| 6 | + 3. Factura de cliente con Pos. Fiscal con tax_ids explícitos → _get_computed_taxes() aplica IVA 0% reemplazando IVA 21% |
| 7 | + 4. Factura de cliente con Pos. Fiscal sólo percepción → _get_computed_taxes() conserva IVA 21% sin sustituirlo |
| 8 | + 5. Pago de factura de cliente con Pos. Fiscal sólo percepción → monto del pago refleja IVA 21% (1210), no IVA 0% (1000) |
| 9 | +""" |
| 10 | + |
| 11 | +from odoo import Command |
| 12 | +from odoo.addons.l10n_ar.tests.common import TestArCommon |
| 13 | +from odoo.tests import tagged |
| 14 | + |
| 15 | + |
| 16 | +@tagged("-at_install", "post_install") |
| 17 | +class TestMapTaxFiscalPosition(TestArCommon): |
| 18 | + """Tests de map_tax para posiciones fiscales de percepción/retención.""" |
| 19 | + |
| 20 | + @classmethod |
| 21 | + def setUpClass(cls): |
| 22 | + super().setUpClass() |
| 23 | + |
| 24 | + # Impuesto de percepción IIBB CABA para usar en l10n_ar_tax_ids |
| 25 | + cls.caba_perception_tax = cls.env.ref("account.%i_ri_tax_percepcion_iibb_caba_aplicada" % cls.env.company.id) |
| 26 | + |
| 27 | + # Posición fiscal "percepción-only": sin tax_ids, con l10n_ar_tax_ids |
| 28 | + cls.perception_only_fp = cls.env["account.fiscal.position"].create( |
| 29 | + { |
| 30 | + "name": "Test FP Percepcion Only", |
| 31 | + "company_id": cls.company_ri.id, |
| 32 | + } |
| 33 | + ) |
| 34 | + cls.env["account.fiscal.position.l10n_ar_tax"].create( |
| 35 | + { |
| 36 | + "fiscal_position_id": cls.perception_only_fp.id, |
| 37 | + "default_tax_id": cls.caba_perception_tax.id, |
| 38 | + "tax_type": "perception", |
| 39 | + } |
| 40 | + ) |
| 41 | + |
| 42 | + # Posición fiscal con tax_ids explícitos: IVA 21% → IVA 0% |
| 43 | + # En Odoo 19, tax_ids es Many2many a account.tax y el mapeo funciona |
| 44 | + # mediante original_tax_ids en el impuesto destino. |
| 45 | + cls.tax_0.original_tax_ids = [Command.set(cls.tax_21.ids)] |
| 46 | + cls.fp_with_tax_mapping = cls.env["account.fiscal.position"].create( |
| 47 | + { |
| 48 | + "name": "Test FP Con Mapping IVA", |
| 49 | + "company_id": cls.company_ri.id, |
| 50 | + "tax_ids": [Command.set(cls.tax_0.ids)], |
| 51 | + } |
| 52 | + ) |
| 53 | + |
| 54 | + def test_map_tax_perception_only_not_affected_by_domestic_fp_substitution(self): |
| 55 | + """ |
| 56 | + Incluso si el domestic FP tiene una sustitución IVA 21% → IVA 0%, |
| 57 | + la FP con sólo percepción debe devolver IVA 21% sin cambios. |
| 58 | + """ |
| 59 | + # Configurar domestic FP con sustitución IVA 21% → IVA 0% |
| 60 | + domestic_fp = self.company_ri.domestic_fiscal_position_id |
| 61 | + if not domestic_fp: |
| 62 | + self.skipTest("No se encontró domestic fiscal position para la compañía de test.") |
| 63 | + |
| 64 | + # Agregar sustitución al domestic FP via original_tax_ids en IVA 0% |
| 65 | + if self.tax_21 not in self.tax_0.original_tax_ids: |
| 66 | + self.tax_0.original_tax_ids = [Command.link(self.tax_21.id)] |
| 67 | + if self.tax_0 not in domestic_fp.tax_ids: |
| 68 | + domestic_fp.tax_ids = [Command.link(self.tax_0.id)] |
| 69 | + |
| 70 | + taxes = self.tax_21 |
| 71 | + result = self.perception_only_fp.map_tax(taxes) |
| 72 | + self.assertEqual( |
| 73 | + result, |
| 74 | + self.tax_21, |
| 75 | + "La FP percepcion-only no debe aplicar la sustitucion IVA 21%→IVA 0% del domestic FP.", |
| 76 | + ) |
| 77 | + |
| 78 | + def test_map_tax_fp_with_explicit_tax_ids_applies_substitution(self): |
| 79 | + """ |
| 80 | + Una FP con tax_ids explícitos debe aplicar la sustitución mediante super().map_tax(). |
| 81 | + """ |
| 82 | + taxes = self.tax_21 |
| 83 | + result = self.fp_with_tax_mapping.map_tax(taxes) |
| 84 | + self.assertEqual( |
| 85 | + result, |
| 86 | + self.tax_0, |
| 87 | + "Una FP con tax_ids explícitos debe aplicar la sustitucion de impuestos.", |
| 88 | + ) |
| 89 | + |
| 90 | + def test_invoice_with_fp_tax_mapping_applies_vat_substitution(self): |
| 91 | + """ |
| 92 | + Al crear una factura con una FP que tiene tax_ids explícitos (IVA 21% → IVA 0%), |
| 93 | + sin tax_ids en la línea, _compute_tax_ids → _get_computed_taxes() → map_tax() |
| 94 | + debe sustituir IVA 21% por IVA 0%. |
| 95 | + Valida que el flujo de impuestos en facturas de cliente aplica correctamente la sustitución |
| 96 | + definida en la FP con tax_ids explícitos.""" |
| 97 | + invoice = self.env["account.move"].create( |
| 98 | + { |
| 99 | + "move_type": "out_invoice", |
| 100 | + "partner_id": self.res_partner_adhoc.id, |
| 101 | + "fiscal_position_id": self.fp_with_tax_mapping.id, |
| 102 | + "company_id": self.company_ri.id, |
| 103 | + "invoice_date": "2025-01-15", |
| 104 | + "date": "2025-01-15", |
| 105 | + "invoice_line_ids": [ |
| 106 | + Command.create( |
| 107 | + { |
| 108 | + "product_id": self.service_iva_21.id, |
| 109 | + "quantity": 1, |
| 110 | + "price_unit": 1000.0, |
| 111 | + } |
| 112 | + ) |
| 113 | + ], |
| 114 | + "l10n_latam_document_number": "0001-00000002", |
| 115 | + } |
| 116 | + ) |
| 117 | + invoice.action_post() |
| 118 | + |
| 119 | + line_taxes = invoice.invoice_line_ids.tax_ids |
| 120 | + self.assertIn( |
| 121 | + self.tax_0, |
| 122 | + line_taxes, |
| 123 | + "IVA 0% debe estar en la línea: la FP con tax_ids debe sustituir IVA 21% por IVA 0%.", |
| 124 | + ) |
| 125 | + self.assertNotIn( |
| 126 | + self.tax_21, |
| 127 | + line_taxes, |
| 128 | + "IVA 21% debe haber sido reemplazado por IVA 0% via la FP con mapping explícito.", |
| 129 | + ) |
| 130 | + |
| 131 | + def test_invoice_with_perception_only_fp_preserves_vat_taxes(self): |
| 132 | + """ |
| 133 | + Al crear una factura con una FP con sólo percepción sin tax_ids explícitos en la línea, |
| 134 | + Odoo computa tax_ids vía _compute_tax_ids → _get_computed_taxes() → map_tax(). |
| 135 | + El IVA 21% del producto debe conservarse sin ser reemplazado por IVA 0%. |
| 136 | + """ |
| 137 | + domestic_fp = self.company_ri.domestic_fiscal_position_id |
| 138 | + if not domestic_fp: |
| 139 | + self.skipTest("No se encontró domestic fiscal position para la compañía de test.") |
| 140 | + |
| 141 | + # Asegurar que el domestic FP tiene sustitución IVA 21% → IVA 0% |
| 142 | + if self.tax_21 not in self.tax_0.original_tax_ids: |
| 143 | + self.tax_0.original_tax_ids = [Command.link(self.tax_21.id)] |
| 144 | + if self.tax_0 not in domestic_fp.tax_ids: |
| 145 | + domestic_fp.tax_ids = [Command.link(self.tax_0.id)] |
| 146 | + |
| 147 | + invoice = self.env["account.move"].create( |
| 148 | + { |
| 149 | + "move_type": "out_invoice", |
| 150 | + "partner_id": self.res_partner_adhoc.id, |
| 151 | + "fiscal_position_id": self.perception_only_fp.id, |
| 152 | + "company_id": self.company_ri.id, |
| 153 | + "invoice_date": "2025-01-15", |
| 154 | + "date": "2025-01-15", |
| 155 | + "invoice_line_ids": [ |
| 156 | + Command.create( |
| 157 | + { |
| 158 | + "product_id": self.service_iva_21.id, |
| 159 | + "quantity": 1, |
| 160 | + "price_unit": 1000.0, |
| 161 | + } |
| 162 | + ) |
| 163 | + ], |
| 164 | + "l10n_latam_document_number": "0001-00000001", |
| 165 | + } |
| 166 | + ) |
| 167 | + invoice.action_post() |
| 168 | + |
| 169 | + line_taxes = invoice.invoice_line_ids.tax_ids |
| 170 | + self.assertIn( |
| 171 | + self.tax_21, |
| 172 | + line_taxes, |
| 173 | + "IVA 21% debe estar presente en la línea cuando se usa una FP percepcion-only.", |
| 174 | + ) |
| 175 | + self.assertNotIn( |
| 176 | + self.tax_0, |
| 177 | + line_taxes, |
| 178 | + "IVA 0% no debe aparecer en la línea; la FP percepcion-only no debe sustituir impuestos.", |
| 179 | + ) |
| 180 | + |
| 181 | + def test_payment_for_invoice_with_perception_only_fp_uses_correct_tax_amount(self): |
| 182 | + """ |
| 183 | + Al registrar el pago de una factura de cliente con FP percepción-only, |
| 184 | + el monto del pago debe reflejar IVA 21% (base 1000 → total 1210), no IVA 0% (1000). |
| 185 | + Si map_tax() hubiera aplicado la sustitución del domestic FP, el total de la |
| 186 | + factura sería 1000 y el pago por 1210 dejaría un residual, o el pago se |
| 187 | + registraría por 1000 y el total sería incorrecto. |
| 188 | + """ |
| 189 | + domestic_fp = self.company_ri.domestic_fiscal_position_id |
| 190 | + if not domestic_fp: |
| 191 | + self.skipTest("No se encontró domestic fiscal position para la compañía de test.") |
| 192 | + |
| 193 | + # Asegurar sustitución activa en domestic FP para que el escenario sea realista |
| 194 | + if self.tax_21 not in self.tax_0.original_tax_ids: |
| 195 | + self.tax_0.original_tax_ids = [Command.link(self.tax_21.id)] |
| 196 | + if self.tax_0 not in domestic_fp.tax_ids: |
| 197 | + domestic_fp.tax_ids = [Command.link(self.tax_0.id)] |
| 198 | + |
| 199 | + invoice = self.env["account.move"].create( |
| 200 | + { |
| 201 | + "move_type": "out_invoice", |
| 202 | + "partner_id": self.res_partner_adhoc.id, |
| 203 | + "fiscal_position_id": self.perception_only_fp.id, |
| 204 | + "company_id": self.company_ri.id, |
| 205 | + "invoice_date": "2025-01-15", |
| 206 | + "date": "2025-01-15", |
| 207 | + "invoice_line_ids": [ |
| 208 | + Command.create( |
| 209 | + { |
| 210 | + "product_id": self.service_iva_21.id, |
| 211 | + "quantity": 1, |
| 212 | + "price_unit": 1000.0, |
| 213 | + } |
| 214 | + ) |
| 215 | + ], |
| 216 | + "l10n_latam_document_number": "0001-00000003", |
| 217 | + } |
| 218 | + ) |
| 219 | + invoice.action_post() |
| 220 | + |
| 221 | + self.assertAlmostEqual( |
| 222 | + invoice.amount_total, |
| 223 | + 1210.0, |
| 224 | + places=2, |
| 225 | + msg="El total de la factura debe incluir IVA 21% (1000 + 210 = 1210).", |
| 226 | + ) |
| 227 | + |
| 228 | + ( |
| 229 | + self.env["account.payment.register"] |
| 230 | + .with_context(active_model="account.move", active_ids=invoice.ids) |
| 231 | + .create({"payment_date": "2025-01-15"}) |
| 232 | + .action_create_payments() |
| 233 | + ) |
| 234 | + |
| 235 | + self.assertAlmostEqual( |
| 236 | + invoice.amount_residual, |
| 237 | + 0.0, |
| 238 | + places=2, |
| 239 | + msg="La factura debe quedar completamente saldada con el monto IVA 21% (1210).", |
| 240 | + ) |
0 commit comments