[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 <jco@odoo.com>
Signed-off-by: Daniel Kosky (dako) <dako@odoo.com>
This commit is contained in:
dbkosky
2022-04-21 19:41:58 +02:00
committed by dbkosky
parent aafe2e35e7
commit 27686d0b0a
4 changed files with 403 additions and 4 deletions
@@ -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,
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_edi_xml
@@ -0,0 +1,73 @@
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd" versione="FPR12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>___ignore___</ProgressivoInvio>
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
<CodiceDestinatario>0000000</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
<RegimeFiscale>RF01</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>00465840031</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Nome>Alessi</Nome>
<Cognome></Cognome>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>Via Privata Alessi 6 </Indirizzo>
<CAP>28887</CAP>
<Comune>Milan</Comune>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD01</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-24</Data>
<Numero>___ignore___</Numero>
</DatiGeneraliDocumento>
</DatiGenerali>
<DatiBeniServizi>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP02</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
<ImportoPagamento></ImportoPagamento>
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -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),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>something price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>something else price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>something not price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<Arrotondamento>-0.04909091</Arrotondamento>
<ImponibileImporto>1312.09</ImponibileImporto>
<Imposta>288.66</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
2577.29
</xpath>
''')
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, "<xpath expr='.//Allegati' position='replace'/>")
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),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>no discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>special discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>50.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>400.20</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>an offer you can't refuse</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>1200.60</ImponibileImporto>
<Imposta>264.13</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
1464.73
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
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),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>nothing shady just a gift for my friend</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>0.00</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
0.00
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)