# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime from lxml import etree from odoo.tests import tagged from odoo.addons.l10n_it_edi.tests.common import TestItEdi from odoo.exceptions import UserError @tagged('post_install_l10n', 'post_install', '-at_install') class TestItEdiExport(TestItEdi): @classmethod def setUpClass(cls): super().setUpClass() cls.price_included_tax = cls.env['account.tax'].create({ 'name': '22% price included tax', 'amount': 22.0, 'amount_type': 'percent', 'price_include': True, 'include_base_amount': True, 'company_id': cls.company.id, }) cls.tax_10 = cls.env['account.tax'].create({ 'name': '10% tax', 'amount': 10.0, 'amount_type': 'percent', 'company_id': cls.company.id, }) cls.tax_zero_percent_hundred_percent_repartition = cls.env['account.tax'].create({ 'name': 'all of nothing', 'amount': 0, 'amount_type': 'percent', 'company_id': cls.company.id, 'invoice_repartition_line_ids': [ (0, 0, {'factor_percent': 100, 'repartition_type': 'base'}), (0, 0, {'factor_percent': 100, 'repartition_type': 'tax'}), ], 'refund_repartition_line_ids': [ (0, 0, {'factor_percent': 100, 'repartition_type': 'base'}), (0, 0, {'factor_percent': 100, 'repartition_type': 'tax'}), ], }) cls.tax_zero_percent_zero_percent_repartition = cls.env['account.tax'].create({ 'name': 'none of nothing', 'amount': 0, 'amount_type': 'percent', 'company_id': cls.company.id, 'invoice_repartition_line_ids': [ (0, 0, {'factor_percent': 100, 'repartition_type': 'base'}), (0, 0, {'factor_percent': 0, 'repartition_type': 'tax'}), ], 'refund_repartition_line_ids': [ (0, 0, {'factor_percent': 100, 'repartition_type': 'base'}), (0, 0, {'factor_percent': 0, 'repartition_type': 'tax'}), ], }) cls.italian_partner_b = cls.env['res.partner'].create({ 'name': 'pa partner', 'vat': 'IT06655971007', 'l10n_it_codice_fiscale': '06655971007', 'l10n_it_pa_index': '123456', 'country_id': cls.env.ref('base.it').id, 'street': 'Via Test PA', 'zip': '32121', 'city': 'PA Town', 'is_company': True }) cls.italian_partner_no_address_codice = cls.env['res.partner'].create({ 'name': 'Alessi', 'l10n_it_codice_fiscale': '00465840031', 'is_company': True, }) cls.italian_partner_no_address_VAT = cls.env['res.partner'].create({ 'name': 'Alessi', 'vat': 'IT00465840031', 'is_company': True, }) cls.american_partner = cls.env['res.partner'].create({ 'name': 'Alessi', 'vat': '00465840031', 'country_id': cls.env.ref('base.us').id, 'is_company': True, }) cls.standard_line_below_400 = { 'name': 'cheap_line', 'quantity': 1, 'price_unit': 100.00, 'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])] } cls.standard_line_400 = { 'name': '400_line', 'quantity': 1, 'price_unit': 327.87, 'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])] } cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_a.id, 'partner_bank_id': cls.test_bank.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, 'name': 'something price included', 'tax_ids': [(6, 0, [cls.price_included_tax.id])] }), (0, 0, { **cls.standard_line, 'name': 'something else price included', 'tax_ids': [(6, 0, [cls.price_included_tax.id])] }), (0, 0, { **cls.standard_line, 'name': 'something not price included', }), ], }) cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_a.id, 'partner_bank_id': cls.test_bank.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, 'name': 'no discount', }), (0, 0, { **cls.standard_line, 'name': 'special discount', 'discount': 50, }), (0, 0, { **cls.standard_line, 'name': "an offer you can't refuse", 'discount': 100, }), ], }) cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_a.id, 'partner_bank_id': cls.test_bank.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, 'name': 'nothing shady just a gift for my friend', 'discount': 100, }), ], }) cls.non_latin_and_latin_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_a.id, 'partner_bank_id': cls.test_bank.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, 'name': 'ʢ◉ᴥ◉ʡ', }), (0, 0, { **cls.standard_line, 'name': '–-', }), (0, 0, { **cls.standard_line, 'name': 'this should be the same as it was', }), ], }) cls.below_400_codice_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_no_address_codice.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line_below_400, }), (0, 0, { **cls.standard_line_below_400, 'name': 'cheap_line_2', 'quantity': 2, 'price_unit': 10.0, }), ], }) cls.total_400_VAT_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_no_address_VAT.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line_400, }), ], }) cls.more_400_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_no_address_codice.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, }), ], }) cls.non_domestic_simplified_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.american_partner.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line_below_400, }), ], }) cls.zero_tax_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_a.id, 'partner_bank_id': cls.test_bank.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, 'name': 'line with tax of 0% with repartition line of 100% ', 'tax_ids': [(6, 0, [cls.tax_zero_percent_hundred_percent_repartition.id])], }), (0, 0, { **cls.standard_line, 'name': 'line with tax of 0% with repartition line of 0% ', 'tax_ids': [(6, 0, [cls.tax_zero_percent_zero_percent_repartition.id])], }), ], }) cls.negative_price_invoice = cls.env['account.move'].with_company(cls.company).create({ 'move_type': 'out_invoice', 'invoice_date': datetime.date(2022, 3, 24), 'invoice_date_due': datetime.date(2022, 3, 24), 'partner_id': cls.italian_partner_a.id, 'partner_bank_id': cls.test_bank.id, 'invoice_line_ids': [ (0, 0, { **cls.standard_line, }), (0, 0, { **cls.standard_line, 'name': 'negative_line', 'price_unit': -100.0, }), (0, 0, { **cls.standard_line, 'name': 'negative_line_different_tax', 'price_unit': -50.0, 'tax_ids': [(6, 0, [cls.tax_10.id])] }), ], }) cls.negative_price_credit_note = cls.negative_price_invoice.with_company(cls.company)._reverse_moves([{ 'invoice_date': datetime.date(2022, 3, 24), }]) # post the invoices cls.price_included_invoice._post() cls.partial_discount_invoice._post() cls.full_discount_invoice._post() cls.non_latin_and_latin_invoice._post() cls.below_400_codice_simplified_invoice._post() cls.total_400_VAT_simplified_invoice._post() cls.zero_tax_invoice._post() cls.negative_price_invoice._post() cls.negative_price_credit_note._post() def test_price_included_taxes(self): """ When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also taxable base = sum(subtotals) + rounding amount) """ # In this case, the first two lines use a price_include tax the # subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564.., # where 22.0 is the tax rate. # # Since the subtotals are rounded we actually have 656.07 lines = self.price_included_invoice.line_ids price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax) self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07]) # So the taxable a base the edi expects (for this tax) is actually 1312.14 price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax) self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14) # The tax amount of the price included tax should be: # per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33 # tax amount: 144.33 * 2 = 288.66 self.assertEqual(price_included_tax_line.amount_currency, -288.66) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), ''' 1 something price included 1.00 656.070000 656.07 22.00 2 something else price included 1.00 656.070000 656.07 22.00 3 something not price included 1.00 800.400000 800.40 22.00 22.00 -0.04909091 1312.09 288.66 I 22.00 800.40 176.09 I 2577.29 2577.29 ''') invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.price_included_invoice)) # Remove the attachment and its details invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_partially_discounted_invoice(self): # The EDI can account for discounts, but a line with, for example, a 100% discount should still have # a corresponding tax with a base amount of 0 invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.partial_discount_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), ''' 1 no discount 1.00 800.400000 800.40 22.00 2 special discount 1.00 800.400000 SC 50.00 400.20 22.00 3 an offer you can't refuse 1.00 800.400000 SC 100.00 0.00 22.00 22.00 1200.60 264.13 I 1464.73 1464.73 ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_fully_discounted_inovice(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.full_discount_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), ''' 1 nothing shady just a gift for my friend 1.00 800.400000 SC 100.00 0.00 22.00 22.00 0.00 0.00 I 0.00 0.00 ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_non_latin_and_latin_invoice(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.non_latin_and_latin_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), ''' 1 ????? 1.00 800.400000 800.40 22.00 2 ?- 1.00 800.400000 800.40 22.00 3 this should be the same as it was 1.00 800.400000 800.40 22.00 22.00 2401.20 528.26 I 2929.46 2929.46 ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_below_400_codice_simplified_invoice(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.below_400_codice_simplified_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_simplified_basis_xml), ''' 00465840031 cheap_line 122.00 22.00 cheap_line_2 24.40 4.40 ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_total_400_VAT_simplified_invoice(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.total_400_VAT_simplified_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_simplified_basis_xml), ''' IT 00465840031 400_line 400.00 72.13 ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_more_400_simplified_invoice(self): with self.assertRaises(UserError): self.more_400_simplified_invoice._post() def test_non_domestic_simplified_invoice(self): with self.assertRaises(UserError): self.non_domestic_simplified_invoice._post() def test_zero_percent_taxes(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.zero_tax_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), ''' 1 line with tax of 0% with repartition line of 100% 1.00 800.400000 800.40 0.00 2 line with tax of 0% with repartition line of 0% 1.00 800.400000 800.40 0.00 0.00 800.40 0.00 I 0.00 800.40 0.00 I 1600.80 1600.80 ''' ) invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_negative_price_invoice(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.negative_price_invoice)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), ''' 1 standard_line 1.00 800.400000 800.40 22.00 2 negative_line 1.00 -100.000000 -100.00 22.00 3 negative_line_different_tax 1.00 -50.000000 -50.00 10.00 22.00 700.40 154.09 I 10.00 -50.00 -5.00 I 799.49 799.49 ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree) def test_negative_price_credit_note(self): invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.negative_price_credit_note)) expected_etree = self.with_applied_xpath( etree.fromstring(self.edi_basis_xml), f''' TD04 799.49 {self.negative_price_invoice.name} {self.negative_price_credit_note.invoice_date} 1 standard_line 1.00 800.400000 800.40 22.00 2 negative_line 1.00 -100.000000 -100.00 22.00 3 negative_line_different_tax 1.00 -50.000000 -50.00 10.00 22.00 700.40 154.09 I 10.00 -50.00 -5.00 I ''') invoice_etree = self.with_applied_xpath(invoice_etree, "") self.assertXmlTreeEqual(invoice_etree, expected_etree)