1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_ec_edi/tests/test_edi_xml.py
2024-12-10 09:04:09 +07:00

842 lines
39 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from lxml import etree
from odoo import Command
from odoo.tests import tagged
from .common import (L10N_EC_EDI_XML_CREDIT_NOTE, L10N_EC_EDI_XML_DEBIT_NOTE,
L10N_EC_EDI_XML_IN_WTH, L10N_EC_EDI_XML_OUT_INV,
L10N_EC_EDI_XML_PURCHASE_LIQ,
L10N_EC_EDI_XML_PURCHASE_LIQ_WTH,
L10N_EC_EDI_XPATH_INVOICE_IN,
L10N_EC_EDI_XPATH_INVOICE_IN_CUSTOM_TAXPAYER,
TestEcEdiCommon)
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestEcEdiXmls(TestEcEdiCommon):
# ===== CUSTOMER INVOICES =====
def test_xml_tree_out_invoice_basic(self, invoice_line_args=None, xpath=None):
out_invoice = self.get_invoice({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
}, invoice_line_args=invoice_line_args)
self.assert_xml_tree_equal(out_invoice, L10N_EC_EDI_XML_OUT_INV, xpath=xpath)
def test_xml_tree_out_05_invoice_basic(self):
line_vals = self.get_invoice_line_vals(vat_tax_xmlid='tax_vat_05_510_sup_01')
self.test_xml_tree_out_invoice_basic(invoice_line_args=line_vals, xpath="""
<xpath expr="//totalConImpuestos/totalImpuesto" position="replace">
<totalImpuesto>
<codigo>2</codigo>
<codigoPorcentaje>5</codigoPorcentaje>
<baseImponible>400.000000</baseImponible>
<tarifa>5.000000</tarifa>
<valor>20.00</valor>
</totalImpuesto>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>420.00</importeTotal>
</xpath>
<xpath expr="//pago/total" position="replace">
<total>420.00</total>
</xpath>
<xpath expr="//detalle/impuestos/impuesto" position="replace">
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>5</codigoPorcentaje>
<tarifa>5.000000</tarifa>
<baseImponible>400.000000</baseImponible>
<valor>20.00</valor>
</impuesto>
</xpath>
""")
def test_xml_tree_out_15_invoice_basic(self):
line_vals = self.get_invoice_line_vals(vat_tax_xmlid='tax_vat_15_510_sup_01')
self.test_xml_tree_out_invoice_basic(
invoice_line_args=line_vals,
xpath="""
<xpath expr="//totalConImpuestos/totalImpuesto" position="replace">
<totalImpuesto>
<codigo>2</codigo>
<codigoPorcentaje>4</codigoPorcentaje>
<baseImponible>400.000000</baseImponible>
<tarifa>15.000000</tarifa>
<valor>60.00</valor>
</totalImpuesto>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>460.00</importeTotal>
</xpath>
<xpath expr="//pago/total" position="replace">
<total>460.00</total>
</xpath>
<xpath expr="//detalle/impuestos/impuesto" position="replace">
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>4</codigoPorcentaje>
<tarifa>15.000000</tarifa>
<baseImponible>400.000000</baseImponible>
<valor>60.00</valor>
</impuesto>
</xpath>
""")
def test_xml_tree_out_invoice_tax_included(self):
"""Checks the XML of the basic invoice when a tax is modified to be included in price."""
self.env['decimal.precision'].search([('name', '=', 'Product Price')]).digits = 5
self._get_tax_by_xml_id('tax_vat_510_sup_01').price_include = True
self.test_xml_tree_out_invoice_basic(xpath="""
<xpath expr="//totalSinImpuestos" position="replace">
<totalSinImpuestos>357.140000</totalSinImpuestos>
</xpath>
<xpath expr="//totalDescuento" position="replace">
<totalDescuento>89.29</totalDescuento>
</xpath>
<xpath expr="//totalImpuesto/baseImponible" position="replace">
<baseImponible>357.140000</baseImponible>
</xpath>
<xpath expr="//totalImpuesto/valor" position="replace">
<valor>42.86</valor>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>400.00</importeTotal>
</xpath>
<xpath expr="//pago/total" position="replace">
<total>400.00</total>
</xpath>
<xpath expr="//detalle" position="replace">
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_a</descripcion>
<cantidad>5.000000</cantidad>
<precioUnitario>89.285714</precioUnitario>
<descuento>89.29</descuento>
<precioTotalSinImpuesto>357.14</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>2</codigoPorcentaje>
<tarifa>12.000000</tarifa>
<baseImponible>357.140000</baseImponible>
<valor>42.86</valor>
</impuesto>
</impuestos>
</detalle>
</xpath>
""")
def test_xml_tree_out_invoice_richer(self, xpath=""):
"""Checks the XML of an invoice with
- 2 lines with the same product, but different taxes (one is tax included in price)
- non-integer quantities
- discounts
"""
line_vals = self.get_invoice_line_vals()
line_vals.extend([Command.create({
'product_id': self.product_b.id,
'price_unit': 1.23,
'quantity': 12.12,
'tax_ids': [Command.set(self._get_tax_by_xml_id('tax_vat_444').ids)],
}), Command.create({
'product_id': self.product_b.id,
'price_unit': 0.12,
'quantity': 120,
'discount': 21,
'tax_ids': [Command.set(self._get_tax_by_xml_id('tax_vat_412').ids)],
})])
self._get_tax_by_xml_id('tax_vat_412').amount_type = 'division' # tax included in price
self.test_xml_tree_out_invoice_basic(invoice_line_args=line_vals, xpath=xpath or """
<xpath expr="//totalSinImpuestos" position="replace">
<totalSinImpuestos>426.290000</totalSinImpuestos>
</xpath>
<xpath expr="//totalDescuento" position="replace">
<totalDescuento>103.03</totalDescuento>
</xpath>
<xpath expr="//totalImpuesto/baseImponible" position="replace">
<baseImponible>426.290000</baseImponible>
</xpath>
<xpath expr="//totalImpuesto/valor" position="replace">
<valor>51.34</valor>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>477.63</importeTotal>
</xpath>
<xpath expr="//pagos/pago" position="replace">
<pago>
<formaPago>16</formaPago>
<total>477.63</total>
<plazo>0</plazo>
<unidadTiempo>dias</unidadTiempo>
</pago>
</xpath>
<xpath expr="//detalles/detalle" position="after">
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_b</descripcion>
<cantidad>12.120000</cantidad>
<precioUnitario>1.230000</precioUnitario>
<descuento>0.00</descuento>
<precioTotalSinImpuesto>14.91</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>2</codigoPorcentaje>
<tarifa>12.000000</tarifa>
<baseImponible>14.910000</baseImponible>
<valor>1.79</valor>
</impuesto>
</impuestos>
</detalle>
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_b</descripcion>
<cantidad>120.000000</cantidad>
<precioUnitario>0.120000</precioUnitario>
<descuento>3.03</descuento>
<precioTotalSinImpuesto>11.38</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>2</codigoPorcentaje>
<tarifa>12.000000</tarifa>
<baseImponible>11.380000</baseImponible>
<valor>1.55</valor>
</impuesto>
</impuestos>
</detalle>
</xpath>
""")
def test_xml_tree_out_invoice_multicurrency(self):
"""Checks the XML of the 'richer' invoice when created in another currency than USD.
In EC, USD is the official currency and the govt expects it to be used in XMLs."""
currency_euro = self.env.ref('base.EUR')
currency_euro.active = True
self.env['res.currency.rate'].create({
'name': self.frozen_today,
'company_rate': 0.5,
'currency_id': currency_euro.id,
'company_id': self.env.company.id,
})
for journal in (self.company_data['default_journal_sale'], self.company_data['default_journal_purchase']):
journal.currency_id = currency_euro.id
self.test_xml_tree_out_invoice_richer(xpath="""
<xpath expr="//totalSinImpuestos" position="replace">
<totalSinImpuestos>852.580000</totalSinImpuestos>
</xpath>
<xpath expr="//totalDescuento" position="replace">
<totalDescuento>206.06</totalDescuento>
</xpath>
<xpath expr="//totalImpuesto/baseImponible" position="replace">
<baseImponible>852.580000</baseImponible>
</xpath>
<xpath expr="//totalImpuesto/valor" position="replace">
<valor>102.68</valor>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>955.26</importeTotal>
</xpath>
<xpath expr="//pagos/pago" position="replace">
<pago>
<formaPago>16</formaPago>
<total>955.26</total>
<plazo>0</plazo>
<unidadTiempo>dias</unidadTiempo>
</pago>
</xpath>
<xpath expr="//detalles/detalle" position="replace">
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_a</descripcion>
<cantidad>5.000000</cantidad>
<precioUnitario>200.000000</precioUnitario>
<descuento>200.00</descuento>
<precioTotalSinImpuesto>800.00</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>2</codigoPorcentaje>
<tarifa>12.000000</tarifa>
<baseImponible>800.000000</baseImponible>
<valor>96.00</valor>
</impuesto>
</impuestos>
</detalle>
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_b</descripcion>
<cantidad>12.120000</cantidad>
<precioUnitario>2.460000</precioUnitario>
<descuento>0.00</descuento>
<precioTotalSinImpuesto>29.82</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>2</codigoPorcentaje>
<tarifa>12.000000</tarifa>
<baseImponible>29.820000</baseImponible>
<valor>3.58</valor>
</impuesto>
</impuestos>
</detalle>
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_b</descripcion>
<cantidad>120.000000</cantidad>
<precioUnitario>0.240000</precioUnitario>
<descuento>6.06</descuento>
<precioTotalSinImpuesto>22.76</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>2</codigoPorcentaje>
<tarifa>12.000000</tarifa>
<baseImponible>22.760000</baseImponible>
<valor>3.10</valor>
</impuesto>
</impuestos>
</detalle>
</xpath>
""")
def test_xml_tree_negative_lines(self):
product = self.product_a
tax_15 = self._get_tax_by_xml_id('tax_vat_15_411_services')
tax_0 = self._get_tax_by_xml_id('tax_vat_415_goods')
invoice_line_args = [
Command.create({
'product_id': product.id,
'price_unit': 500.0,
'tax_ids': tax_15.ids,
}),
Command.create({
'product_id': product.id,
'price_unit': 200.0,
'tax_ids': (tax_15 + tax_0).ids,
}),
Command.create({
'product_id': product.id,
'price_unit': 300.0,
'tax_ids': (tax_15 + tax_0).ids,
}),
Command.create({
'product_id': product.id,
'price_unit': -400.0,
'tax_ids': (tax_0 + tax_15).ids,
}),
]
self.test_xml_tree_out_invoice_basic(invoice_line_args=invoice_line_args, xpath="""
<xpath expr="//totalSinImpuestos" position="replace">
<totalSinImpuestos>600.000000</totalSinImpuestos>
</xpath>
<xpath expr="//totalDescuento" position="replace">
<totalDescuento>400.00</totalDescuento>
</xpath>
<xpath expr="//totalConImpuestos" position="replace">
<totalConImpuestos>
<totalImpuesto>
<codigo>2</codigo>
<codigoPorcentaje>4</codigoPorcentaje>
<baseImponible>600.000000</baseImponible>
<tarifa>15.000000</tarifa>
<valor>90.00</valor>
</totalImpuesto>
<totalImpuesto>
<codigo>2</codigo>
<codigoPorcentaje>0</codigoPorcentaje>
<baseImponible>100.000000</baseImponible>
<tarifa>0.000000</tarifa>
<valor>0.00</valor>
</totalImpuesto>
</totalConImpuestos>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>690.00</importeTotal>
</xpath>
<xpath expr="//pago/total" position="replace">
<total>690.00</total>
</xpath>
<xpath expr="//detalles/detalle" position="replace">
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_a</descripcion>
<cantidad>1.000000</cantidad>
<precioUnitario>500.000000</precioUnitario>
<descuento>0.00</descuento>
<precioTotalSinImpuesto>500.00</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>4</codigoPorcentaje>
<tarifa>15.000000</tarifa>
<baseImponible>500.000000</baseImponible>
<valor>75.00</valor>
</impuesto>
</impuestos>
</detalle>
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_a</descripcion>
<cantidad>1.000000</cantidad>
<precioUnitario>200.000000</precioUnitario>
<descuento>100.00</descuento>
<precioTotalSinImpuesto>100.00</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>4</codigoPorcentaje>
<tarifa>15.000000</tarifa>
<baseImponible>100.000000</baseImponible>
<valor>15.00</valor>
</impuesto>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>0</codigoPorcentaje>
<tarifa>0.000000</tarifa>
<baseImponible>100.000000</baseImponible>
<valor>0.00</valor>
</impuesto>
</impuestos>
</detalle>
<detalle>
<codigoPrincipal>N/A</codigoPrincipal>
<descripcion>product_a</descripcion>
<cantidad>1.000000</cantidad>
<precioUnitario>300.000000</precioUnitario>
<descuento>300.00</descuento>
<precioTotalSinImpuesto>0.00</precioTotalSinImpuesto>
<impuestos>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>4</codigoPorcentaje>
<tarifa>15.000000</tarifa>
<baseImponible>0.000000</baseImponible>
<valor>0.00</valor>
</impuesto>
<impuesto>
<codigo>2</codigo>
<codigoPorcentaje>0</codigoPorcentaje>
<tarifa>0.000000</tarifa>
<baseImponible>0.000000</baseImponible>
<valor>0.00</valor>
</impuesto>
</impuestos>
</detalle>
</xpath>
""")
# ===== DEBIT & CREDIT NOTES, LIQUIDATIONS =====
def test_xml_tree_debit_note(self):
invoice = self.get_invoice({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
})
invoice.action_post()
debit_note_wizard = self.env['account.debit.note'].with_context(active_model="account.move", active_ids=invoice.ids).create({
'date': self.frozen_today,
'reason': 'no reason',
'copy_lines': True,
})
with freeze_time(self.frozen_today):
debit_note_wizard.create_debit()
debit_note = self.env['account.move'].search([('debit_origin_id', '=', invoice.id)])
debit_note.ensure_one()
self.assert_xml_tree_equal(debit_note, L10N_EC_EDI_XML_DEBIT_NOTE)
def test_xml_tree_credit_note(self):
invoice = self.get_invoice({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
})
invoice.action_post()
credit_note_wizard = self.env['account.move.reversal'].with_context(active_model='account.move', active_ids=invoice.ids).create({
'date': self.frozen_today,
'journal_id': invoice.journal_id.id,
'reason': 'no reason',
})
with freeze_time(self.frozen_today):
credit_note_wizard.modify_moves()
credit_note = self.env['account.move'].search([('reversed_entry_id', '=', invoice.id)])
credit_note.ensure_one()
self.assert_xml_tree_equal(credit_note, L10N_EC_EDI_XML_CREDIT_NOTE, post_move=False)
def test_xml_tree_purchase_liquidation(self):
self.partner_b.country_id = self.env.ref('base.us').id
journal_liq = self.env['account.journal'].search([
('company_id', '=', self.company_data['company'].id),
('code', '=', 'LIQCO')
])
in_invoice = self.get_invoice({
'move_type': 'in_invoice',
'partner_id': self.partner_b.id,
'journal_id': journal_liq.id,
})
self.assert_xml_tree_equal(in_invoice, L10N_EC_EDI_XML_PURCHASE_LIQ)
# ===== WITHHOLDS =====
def test_purchase_liquidation_wth(self):
"""Checks the XML of a withhold on top of a purchase liquidation that has two different
tax supports and a domestic supplier with identification type "cedula"."""
def create_wth_lines(wizard, invoice):
# Create withhold lines manually
self.env['l10n_ec.wizard.account.withhold.line'].create({
'invoice_id': invoice.id,
'wizard_id': wizard.id,
'base': 12,
'tax_id': self._get_tax_by_xml_id('tax_withhold_vat_100').ids[0],
'taxsupport_code': '01',
'amount': 12,
})
self.env['l10n_ec.wizard.account.withhold.line'].create({
'invoice_id': invoice.id,
'wizard_id': wizard.id,
'base': 23.99,
'tax_id': self._get_tax_by_xml_id('tax_withhold_vat_100').ids[0],
'taxsupport_code': '04',
'amount': 23.99,
})
with freeze_time(self.frozen_today):
purchase_liq = self.get_purchase_liq()
purchase_liq.action_post()
withhold = self.get_withhold(purchase_liq, create_wth_lines)
self.assert_xml_tree_equal(withhold, L10N_EC_EDI_XML_PURCHASE_LIQ_WTH, post_move=False)
def test_xml_tree_in_withhold_foreign_partner_dm(self):
"""Checks the XML of a purchase withhold whose invoice originates from a foreign partner."""
self.partner_b.country_id = self.env.ref('base.dm').id
self.partner_b.l10n_latam_identification_type_id = self.env.ref('l10n_latam_base.it_vat')
def create_wth_lines(wizard, invoice):
self.env['l10n_ec.wizard.account.withhold.line'].create({
'invoice_id': invoice.id,
'wizard_id': wizard.id,
'tax_id': self._get_tax_by_xml_id('tax_withhold_profit_502_422').ids[0],
'taxsupport_code': '02',
'base': 400,
'amount': 88, # VAT, 22.00% of 400
})
wizard.foreign_regime = '01'
xpath = self.get_withhold_xpath_for_taxes(tax_percent='22.00', withhold_amount='88.00', tax_code=502)
xpath += """
<xpath expr="//tipoIdentificacionSujetoRetenido" position="replace">
<tipoIdentificacionSujetoRetenido>08</tipoIdentificacionSujetoRetenido>
<tipoSujetoRetenido>01</tipoSujetoRetenido>
</xpath>
<xpath expr="//identificacionSujetoRetenido" position="replace">
<identificacionSujetoRetenido>0453661050</identificacionSujetoRetenido>
</xpath>
<xpath expr="//codSustento" position="replace">
<codSustento>02</codSustento>
</xpath>
<xpath expr="//codDocSustento" position="replace">
<codDocSustento>09</codDocSustento>
</xpath>
<xpath expr="//pagoLocExt" position="replace">
<pagoLocExt>02</pagoLocExt>
<tipoRegi>01</tipoRegi>
<paisEfecPago>136</paisEfecPago>
<aplicConvDobTrib>NO</aplicConvDobTrib>
<pagExtSujRetNorLeg>SI</pagExtSujRetNorLeg>
<pagoRegFis>SI</pagoRegFis>
</xpath>
<xpath expr="//importeTotal" position="replace">
<importeTotal>400.00</importeTotal>
</xpath>
<xpath expr="//impuestosDocSustento/impuestoDocSustento" position="replace">
<impuestoDocSustento>
<codImpuestoDocSustento>2</codImpuestoDocSustento>
<codigoPorcentaje>0</codigoPorcentaje>
<baseImponible>400.00</baseImponible>
<tarifa>0.00</tarifa>
<valorImpuesto>0.00</valorImpuesto>
</impuestoDocSustento>
</xpath>
<xpath expr="//pagos/pago" position="replace">
<pago>
<formaPago>01</formaPago>
<total>400.00</total>
</pago>
</xpath>
"""
invoice_args = {'partner_id': self.partner_b.id}
invoice_line_args = [Command.create({
'product_id': self.product_a.id,
'price_unit': 100.0,
'quantity': 5,
'discount': 20,
'tax_ids': [Command.set(self._get_tax_by_xml_id('tax_vat_518_sup_02').ids)],
})]
self.get_and_test_xml_tree_in_withhold(line_creation_method=create_wth_lines, xpath=xpath,
invoice_args=invoice_args, invoice_line_args=invoice_line_args)
def test_xml_tree_in_withhold_suggested_tax_credit_card(self):
"""Checks the XML of a purchase withhold whose invoice's payment method is a credit card.
Payments with credit/debit/gift cards: tax must be company.l10n_ec_withhold_credit_card_tax_id."""
self.get_and_test_xml_tree_in_withhold(
invoice_args={
'l10n_ec_sri_payment_id': self.env['l10n_ec.sri.payment'].search([('code', '=', 16)], limit=1).id
},
xpath=self.get_withhold_xpath_for_taxes(tax_percent='0.00', tax_code='332G', withhold_amount='0.00', payment_code=16)
)
def test_xml_tree_in_withhold_suggested_tax_fallback_goods(self):
"""Checks the XML of a purchase withhold for goods.
Fallback tax for goods: company.l10n_ec_withhold_goods_tax_id."""
self.get_and_test_xml_tree_in_withhold()
def test_xml_tree_in_withhold_suggested_tax_fallback_services(self):
"""Checks the XML of a purchase withhold for goods.
Fallback tax for services: company.l10n_ec_withhold_services_tax_id."""
self.product_a.type = 'service'
self.get_and_test_xml_tree_in_withhold(
xpath=self.get_withhold_xpath_for_taxes(tax_percent='2.75', withhold_amount='11.00', tax_code=3440)
)
def test_xml_tree_in_withhold_suggested_tax_taxpayer_type(self):
"""Checks the XML of a purchase withhold for RIMPE partner.
Tax for partner with taxpayer type: partner.l10n_ec_taxpayer_type_id.profit_withhold_tax_id."""
self.partner_a.l10n_ec_taxpayer_type_id = self.env.ref('l10n_ec_edi.l10n_ec_taxpayer_type_13')
self.get_and_test_xml_tree_in_withhold(
xpath=self.get_withhold_xpath_for_taxes(tax_percent='1.00', withhold_amount='4.00', tax_code=343))
def test_xml_custom_taxpayer_type_partner_on_purchase_invoice_withhold(self):
"""Checks the XML of a purchase withhold for a partner with a custom taxpayer type"""
self.set_custom_taxpayer_type_on_partner_a()
self.test_xml_withholding_purchase_invoice(custom_xpath=L10N_EC_EDI_XPATH_INVOICE_IN_CUSTOM_TAXPAYER)
def test_xml_tree_in_withhold_manual_taxes(self):
"""Checks the XML of a purchase withhold where lines have been created manually.
Lines are created with different taxes (one VAT and one profit) and tax supports."""
line_vals_1 = self.get_invoice_line_vals()
line_vals_2 = self.get_invoice_line_vals()
line_vals_2[0][2]['tax_ids'] = [Command.set(self._get_tax_by_xml_id('tax_vat_512_sup_07').ids)]
line_vals_3 = self.get_invoice_line_vals()
line_vals_3[0][2]['tax_ids'] = [Command.set(self._get_tax_by_xml_id('tax_vat_510_sup_01').ids)]
def create_wth_lines(wizard, invoice):
wizard_line_cls = self.env['l10n_ec.wizard.account.withhold.line']
# base is chosen so it is accepted for both VAT and profit lines
common_vals = {'invoice_id': invoice.id, 'wizard_id': wizard.id, 'base': 42}
wizard_line_cls.create({
**common_vals,
'tax_id': self._get_tax_by_xml_id('tax_withhold_vat_10').ids[0],
'taxsupport_code': '01',
'amount': 4.2, # VAT, 10% of 42
})
wizard_line_cls.create({
**common_vals,
'tax_id': self._get_tax_by_xml_id('tax_withhold_profit_304D').ids[0],
'taxsupport_code': '07',
'amount': 3.36, # profit, 8% of 42
})
self.get_and_test_xml_tree_in_withhold(
line_creation_method=create_wth_lines,
invoice_line_args=line_vals_1 + line_vals_2 + line_vals_3,
xpath="""
<xpath expr="//docsSustento" position="replace">
<docsSustento>
<docSustento>
<codSustento>01</codSustento>
<codDocSustento>01</codDocSustento>
<numDocSustento>001001000000001</numDocSustento>
<fechaEmisionDocSustento>25/01/2022</fechaEmisionDocSustento>
<pagoLocExt>01</pagoLocExt>
<totalSinImpuestos>800.00</totalSinImpuestos>
<importeTotal>896.00</importeTotal>
<impuestosDocSustento>
<impuestoDocSustento>
<codImpuestoDocSustento>2</codImpuestoDocSustento>
<codigoPorcentaje>2</codigoPorcentaje>
<baseImponible>800.00</baseImponible>
<tarifa>12.00</tarifa>
<valorImpuesto>96.00</valorImpuesto>
</impuestoDocSustento>
</impuestosDocSustento>
<retenciones>
<retencion>
<codigo>2</codigo>
<codigoRetencion>9</codigoRetencion>
<baseImponible>42.00</baseImponible>
<porcentajeRetener>10.00</porcentajeRetener>
<valorRetenido>4.20</valorRetenido>
</retencion>
</retenciones>
<pagos>
<pago>
<formaPago>01</formaPago>
<total>896.00</total>
</pago>
</pagos>
</docSustento>
<docSustento>
<codSustento>07</codSustento>
<codDocSustento>01</codDocSustento>
<numDocSustento>001001000000001</numDocSustento>
<fechaEmisionDocSustento>25/01/2022</fechaEmisionDocSustento>
<pagoLocExt>01</pagoLocExt>
<totalSinImpuestos>400.00</totalSinImpuestos>
<importeTotal>448.00</importeTotal>
<impuestosDocSustento>
<impuestoDocSustento>
<codImpuestoDocSustento>2</codImpuestoDocSustento>
<codigoPorcentaje>2</codigoPorcentaje>
<baseImponible>400.00</baseImponible>
<tarifa>12.00</tarifa>
<valorImpuesto>48.00</valorImpuesto>
</impuestoDocSustento>
</impuestosDocSustento>
<retenciones>
<retencion>
<codigo>1</codigo>
<codigoRetencion>304D</codigoRetencion>
<baseImponible>42.00</baseImponible>
<porcentajeRetener>8.00</porcentajeRetener>
<valorRetenido>3.36</valorRetenido>
</retencion>
</retenciones>
<pagos>
<pago>
<formaPago>01</formaPago>
<total>448.00</total>
</pago>
</pagos>
</docSustento>
</docsSustento>
</xpath>
""")
def test_xml_withholding_purchase_invoice(self, custom_xpath=False):
# test the prebuild xml withhold for purchase invoice
wizard, _purchase_invoice = self.get_wizard_and_purchase_invoice()
with freeze_time(self.frozen_today):
withhold = wizard.action_create_and_post_withhold()
xpath = """
<xpath expr="//tipoIdentificacionSujetoRetenido" position="replace">
<tipoIdentificacionSujetoRetenido>04</tipoIdentificacionSujetoRetenido>
</xpath>
<xpath expr="//identificacionSujetoRetenido" position="replace">
<identificacionSujetoRetenido>0453661050152</identificacionSujetoRetenido>
</xpath>
<xpath expr="//claveAcceso" position="replace">
<claveAcceso>2501202207179236683600110010010000000013121521419</claveAcceso>
</xpath>
"""
xpath += L10N_EC_EDI_XPATH_INVOICE_IN
if custom_xpath:
xpath += custom_xpath
self.assert_xml_tree_equal(withhold, L10N_EC_EDI_XML_IN_WTH, post_move=False, xpath=xpath)
# ===== HELPERS =====
def get_purchase_liq(self):
"""Creates a purchase liquidation with two lines with different tax supports."""
def get_purchase_liq_line_vals():
return [Command.create({
'product_id': self.product_a.id,
'price_unit': 100.0,
'quantity': 1,
'discount': 0,
'tax_ids': [Command.set(self._get_tax_by_xml_id('tax_vat_510_sup_01').ids)],
}), Command.create({
'product_id': self.product_b.id,
'price_unit': 100.0,
'quantity': 2,
'discount': 0,
'tax_ids': [Command.set(self._get_tax_by_xml_id('tax_vat_512_sup_04').ids)],
})]
journal_liq = self.env['account.journal'].search([
('company_id', '=', self.company_data['company'].id),
('code', '=', 'LIQCO')
], limit=1)
purchase_liquidation = self.get_invoice({
'move_type': 'in_invoice',
'partner_id': self.partner_b.id,
'journal_id': journal_liq.id,
'invoice_line_ids': get_purchase_liq_line_vals()
})
return purchase_liquidation
def get_withhold(self, invoice, line_creation_method=None):
"""Creates a withhold on the provided invoice, with an option to create the lines 'manually'."""
# Create wizard
with freeze_time(self.frozen_today):
wizard = self.env['l10n_ec.wizard.account.withhold'].with_context(active_ids=invoice.id, active_model='account.move')
wizard = wizard.create({})
wizard.document_number = '001-001-000000001'
# Add withhold lines
if line_creation_method:
for line in wizard.withhold_line_ids:
line.unlink()
line_creation_method(wizard, invoice)
# Create withhold
withhold = wizard.action_create_and_post_withhold()
return withhold
def get_withhold_xpath_for_taxes(self, tax_percent, tax_code, withhold_amount, payment_code='01'):
"""Provides an xpath modifying a withhold XML in accordance with the provided taxes."""
return f"""
<xpath expr="//retencion" position="replace">
<retencion>
<codigo>1</codigo>
<codigoRetencion>{tax_code}</codigoRetencion>
<baseImponible>400.00</baseImponible>
<porcentajeRetener>{tax_percent}</porcentajeRetener>
<valorRetenido>{withhold_amount}</valorRetenido>
</retencion>
</xpath>
<xpath expr="//pago" position="replace">
<pago>
<formaPago>{payment_code}</formaPago>
<total>448.00</total>
</pago>
</xpath>
"""
def get_and_test_xml_tree_in_withhold(self, line_creation_method=None, invoice_args=None, invoice_line_args=None, xpath=None):
"""Generic method for creating an invoice, adding a related withhold, and checking its generated XML."""
# Create the invoice and withhold
invoice_vals = {
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'l10n_ec_sri_payment_id': self.env['l10n_ec.sri.payment'].search([('code', '=', '01')], limit=1).id
}
if invoice_args:
invoice_vals.update(invoice_args)
in_invoice = self.get_invoice(invoice_vals, invoice_line_args)
in_invoice._post()
residual_before = in_invoice.amount_residual
in_wth = self.get_withhold(in_invoice, line_creation_method)
# Check the generated XML document
self.assert_xml_tree_equal(in_wth, L10N_EC_EDI_XML_IN_WTH, post_move=False, xpath=xpath)
# Check the line reconciliation (posting withhold decreases the invoice's residual amount)
residual_expected = residual_before - sum([line.l10n_ec_withhold_tax_amount for line in in_wth.l10n_ec_withhold_line_ids])
self.assertEqual(residual_expected, in_invoice.amount_residual)
def assert_xml_tree_equal(self, move, xml_string, post_move=True, xpath=None):
with freeze_time(self.frozen_today):
post_move and move.action_post()
move_string, errors = self.env['account.edi.format'].with_context(skip_xsd=True)._l10n_ec_generate_xml(move)
self.assertFalse(errors)
move_xml = etree.fromstring(move_string.encode())
xml_expected = etree.fromstring(xml_string)
if xpath:
xml_expected = self.with_applied_xpath(xml_expected, xpath)
self.assertXmlTreeEqual(move_xml, xml_expected)