# -*- 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="""
2
5
400.000000
5.000000
20.00
420.00
420.00
2
5
5.000000
400.000000
20.00
""")
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="""
2
4
400.000000
15.000000
60.00
460.00
460.00
2
4
15.000000
400.000000
60.00
""")
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="""
357.140000
89.29
357.140000
42.86
400.00
400.00
N/A
product_a
5.000000
89.285714
89.29
357.14
2
2
12.000000
357.140000
42.86
""")
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 """
426.290000
103.03
426.290000
51.34
477.63
16
477.63
0
dias
N/A
product_b
12.120000
1.230000
0.00
14.91
2
2
12.000000
14.910000
1.79
N/A
product_b
120.000000
0.120000
3.03
11.38
2
2
12.000000
11.380000
1.55
""")
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="""
852.580000
206.06
852.580000
102.68
955.26
16
955.26
0
dias
N/A
product_a
5.000000
200.000000
200.00
800.00
2
2
12.000000
800.000000
96.00
N/A
product_b
12.120000
2.460000
0.00
29.82
2
2
12.000000
29.820000
3.58
N/A
product_b
120.000000
0.240000
6.06
22.76
2
2
12.000000
22.760000
3.10
""")
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="""
600.000000
400.00
2
4
600.000000
15.000000
90.00
2
0
100.000000
0.000000
0.00
690.00
690.00
N/A
product_a
1.000000
500.000000
0.00
500.00
2
4
15.000000
500.000000
75.00
N/A
product_a
1.000000
200.000000
100.00
100.00
2
4
15.000000
100.000000
15.00
2
0
0.000000
100.000000
0.00
N/A
product_a
1.000000
300.000000
300.00
0.00
2
4
15.000000
0.000000
0.00
2
0
0.000000
0.000000
0.00
""")
# ===== 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 += """
08
01
0453661050
02
09
02
01
136
NO
SI
SI
400.00
2
0
400.00
0.00
0.00
01
400.00
"""
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="""
01
01
001001000000001
25/01/2022
01
800.00
896.00
2
2
800.00
12.00
96.00
2
9
42.00
10.00
4.20
01
896.00
07
01
001001000000001
25/01/2022
01
400.00
448.00
2
2
400.00
12.00
48.00
1
304D
42.00
8.00
3.36
01
448.00
""")
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 = """
04
0453661050152
2501202207179236683600110010010000000013121521419
"""
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"""
1
{tax_code}
400.00
{tax_percent}
{withhold_amount}
{payment_code}
448.00
"""
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)