From 27686d0b0a4b3b8a034dd95d622a4fd494b0b0bf Mon Sep 17 00:00:00 2001 From: dbkosky Date: Tue, 22 Mar 2022 14:51:35 +0000 Subject: [PATCH] [FIX] l10n_it_edi_sdicoop: add tests for price included taxes and discounted lines Create test directory for l10n_it_edi_sdcioop, and relevant tests for the exported xml. These tests are run in l10n_it_edi_sdicoop because there is different behaviour when l10n_it_edi_sdicoop is installed (different checks are run on the invoice).This module should eventually be merged with l10n_it_edi in master. closes odoo/odoo#89207 X-original-commit: dffb205dad9ce29288c82c4b51011b77ddf33580 Signed-off-by: Josse Colpaert Signed-off-by: Daniel Kosky (dako) --- .../l10n_it_edi/tests/test_ir_mail_server.py | 12 +- addons/l10n_it_edi_sdicoop/tests/__init__.py | 4 + .../expected_xmls/IT00470550013_basis.xml | 73 ++++ .../l10n_it_edi_sdicoop/tests/test_edi_xml.py | 318 ++++++++++++++++++ 4 files changed, 403 insertions(+), 4 deletions(-) create mode 100644 addons/l10n_it_edi_sdicoop/tests/__init__.py create mode 100644 addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml create mode 100644 addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py diff --git a/addons/l10n_it_edi/tests/test_ir_mail_server.py b/addons/l10n_it_edi/tests/test_ir_mail_server.py index 8d3487ccda1..7fd6afb4a1a 100644 --- a/addons/l10n_it_edi/tests/test_ir_mail_server.py +++ b/addons/l10n_it_edi/tests/test_ir_mail_server.py @@ -5,7 +5,7 @@ import datetime import logging from collections import namedtuple from unittest.mock import patch -import freezegun +from freezegun import freeze_time from odoo import tools from odoo.tests import tagged @@ -14,7 +14,6 @@ from odoo.addons.l10n_it_edi.tools.remove_signature import remove_signature _logger = logging.getLogger(__name__) - @tagged('post_install_l10n', 'post_install', '-at_install') class PecMailServerTests(AccountEdiTestCommon): """ Main test class for the l10n_it_edi vendor bills XML import from a PEC mail account""" @@ -51,7 +50,7 @@ class PecMailServerTests(AccountEdiTestCommon): cls.company = cls.company_data_2['company'] # Initialize the company's codice fiscale - cls.company.l10n_it_codice_fiscale = 'IT01234560157' + cls.company.l10n_it_codice_fiscale = '01234560157' # Build test data. # invoice_filename1 is used for vendor bill receipts tests @@ -84,6 +83,11 @@ class PecMailServerTests(AccountEdiTestCommon): 'server_type': 'imap', 'l10n_it_is_pec': True}) + cls.test_invoice_xmls = {k: cls._get_test_file_content(v) for k, v in [ + ('normal_1', 'IT01234567890_FPR01.xml'), + ('signed', 'IT01234567890_FPR01.xml.p7m'), + ]} + @classmethod def _get_test_file_content(cls, filename): """ Get the content of a test file inside this module """ @@ -111,7 +115,7 @@ class PecMailServerTests(AccountEdiTestCommon): def test_receive_signed_vendor_bill(self): """ Test a signed (P7M) sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """ - with freezegun.freeze_time('2020-04-06'): + with freeze_time('2020-04-06'): invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename) self.assertRecordValues(invoices, [{ 'company_id': self.company.id, diff --git a/addons/l10n_it_edi_sdicoop/tests/__init__.py b/addons/l10n_it_edi_sdicoop/tests/__init__.py new file mode 100644 index 00000000000..ef57f8ba3da --- /dev/null +++ b/addons/l10n_it_edi_sdicoop/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_edi_xml diff --git a/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml b/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml new file mode 100644 index 00000000000..56c49b87938 --- /dev/null +++ b/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_basis.xml @@ -0,0 +1,73 @@ + + + + + IT + 01234560157 + + ___ignore___ + FPR12 + 0000000 + + + + + + + IT + 01234560157 + + 01234560157 + + company_2_data + + RF01 + + + 1234 Test Street + 12345 + Prova + IT + + + + + + IT + 00465840031 + + + Alessi + + + + + Via Privata Alessi 6 + 28887 + Milan + IT + + + + + + + TD01 + EUR + 2022-03-24 + ___ignore___ + + + + + + TP02 + + MP05 + 2022-03-24 + + ___ignore___ + + + + diff --git a/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py new file mode 100644 index 00000000000..4aa2bb3ef15 --- /dev/null +++ b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import datetime +import logging +from lxml import etree +from freezegun import freeze_time + +from odoo import tools +from odoo.tests import tagged +from odoo.addons.account_edi.tests.common import AccountEdiTestCommon + +_logger = logging.getLogger(__name__) + +@tagged('post_install_l10n', 'post_install', '-at_install') +class TestItEdi(AccountEdiTestCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass(chart_template_ref='l10n_it.l10n_it_chart_template_generic', + edi_format_ref='l10n_it_edi.edi_fatturaPA') + + # Use the company_data_2 to test that the e-invoice is imported for the right company + cls.company = cls.company_data_2['company'] + + cls.company.l10n_it_codice_fiscale = '01234560157' + cls.company.vat = 'IT01234560157' + cls.test_bank = cls.env['res.partner.bank'].with_company(cls.company).create({ + 'partner_id': cls.company.partner_id.id, + 'acc_number': 'IT1212341234123412341234123', + 'bank_name': 'BIG BANK', + 'bank_bic': 'BIGGBANQ', + }) + cls.company.l10n_it_tax_system = "RF01" + cls.company.street = "1234 Test Street" + cls.company.zip = "12345" + cls.company.city = "Prova" + cls.company.country_id = cls.env.ref('base.it') + + 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.italian_partner_a = cls.env['res.partner'].create({ + 'name': 'Alessi', + 'vat': 'IT00465840031', + 'l10n_it_codice_fiscale': '00465840031', + 'country_id': cls.env.ref('base.it').id, + 'street': 'Via Privata Alessi 6', + 'zip': '28887', + 'city': 'Milan', + 'company_id': cls.company.id, + }) + + cls.standard_line = { + 'name': 'standard_line', + 'quantity': 1, + 'price_unit': 800.40, + '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), + '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), + '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), + '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, + }), + ], + }) + + # We create this because we are unable to post without a proxy user existing + cls.proxy_user = cls.env['account_edi_proxy_client.user'].create({ + 'id_client': 'l10n_it_edi_sdicoop_test', + 'company_id': cls.company.id, + 'edi_format_id': cls.edi_format.id, + 'edi_identification': 'l10n_it_edi_sdicoop_test', + 'private_key': 'l10n_it_edi_sdicoop_test', + }) + + # post the invoices + cls.price_included_invoice._post() + cls.partial_discount_invoice._post() + cls.full_discount_invoice._post() + + cls.edi_basis_xml = cls._get_test_file_content('IT00470550013_basis.xml') + + @classmethod + def _get_test_file_content(cls, filename): + """ Get the content of a test file inside this module """ + path = 'l10n_it_edi_sdicoop/tests/expected_xmls/' + filename + with tools.file_open(path, mode='rb') as test_file: + return test_file.read() + + @freeze_time('2020-03-24') + 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.price_total, 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 + + ''') + invoice_etree = etree.fromstring(self.price_included_invoice._export_as_xml()) + # Remove the attachment and its details + invoice_etree = self.with_applied_xpath(invoice_etree, "") + self.assertXmlTreeEqual(invoice_etree, expected_etree) + + @freeze_time('2020-03-24') + 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.partial_discount_invoice._export_as_xml()) + 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 + + ''') + invoice_etree = self.with_applied_xpath(invoice_etree, "") + self.assertXmlTreeEqual(invoice_etree, expected_etree) + + @freeze_time('2020-03-24') + def test_fully_discounted_inovice(self): + invoice_etree = etree.fromstring(self.full_discount_invoice._export_as_xml()) + 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 + + ''') + invoice_etree = self.with_applied_xpath(invoice_etree, "") + self.assertXmlTreeEqual(invoice_etree, expected_etree)