diff --git a/addons/l10n_it_edi/__manifest__.py b/addons/l10n_it_edi/__manifest__.py
index d4d558654f6..03254cc845c 100644
--- a/addons/l10n_it_edi/__manifest__.py
+++ b/addons/l10n_it_edi/__manifest__.py
@@ -21,6 +21,7 @@ E-invoice implementation
'security/ir.model.access.csv',
'data/account_edi_data.xml',
'data/invoice_it_template.xml',
+ 'data/invoice_it_simplified_template.xml',
'views/l10n_it_view.xml',
],
'demo': [
diff --git a/addons/l10n_it_edi/data/invoice_it_simplified_template.xml b/addons/l10n_it_edi/data/invoice_it_simplified_template.xml
new file mode 100644
index 00000000000..0c1a8428333
--- /dev/null
+++ b/addons/l10n_it_edi/data/invoice_it_simplified_template.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PDF
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
index 4e7659dc791..76379eb4d00 100644
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -59,6 +59,14 @@ class AccountEdiFormat(models.Model):
}
def _l10n_it_edi_check_invoice_configuration(self, invoice):
+ errors = self._l10n_it_edi_check_ordinary_invoice_configuration(invoice)
+
+ if not errors:
+ errors = self._l10n_it_edi_check_simplified_invoice_configuration(invoice)
+
+ return errors
+
+ def _l10n_it_edi_check_ordinary_invoice_configuration(self, invoice):
errors = []
seller = invoice.company_id
buyer = invoice.commercial_partner_id
@@ -98,7 +106,35 @@ class AccountEdiFormat(models.Model):
# <1.4.1>
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
- errors.append(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).", buyer.display_name))
+ errors.append(_("The buyer, %s, or his company must have a VAT number and/or a tax code (Codice Fiscale).", buyer.display_name))
+
+ # <2.2.1>
+ for invoice_line in invoice.invoice_line_ids:
+ if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
+ raise UserError(_("You must select one and only one tax by line."))
+
+ for tax_line in invoice.line_ids.filtered(lambda line: line.tax_line_id):
+ if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
+ errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
+
+ return errors
+
+ def _l10n_it_edi_check_simplified_invoice_configuration(self, invoice):
+ errors = self._l10n_it_edi_check_buyer_invoice_configuration(invoice)
+ template = self.env.ref('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', raise_if_not_found=False)
+
+ if errors and template:
+ buyer = invoice.commercial_partner_id
+ if ((not buyer.country_id or buyer.country_id.code == 'IT')
+ and (buyer.l10n_it_codice_fiscale or (buyer.vat and (buyer.vat[:2].upper() == 'IT' or buyer.vat[:2].isdecimal())))
+ and invoice.amount_total <= 400):
+ return []
+
+ return errors
+
+ def _l10n_it_edi_check_buyer_invoice_configuration(self, invoice):
+ errors = []
+ buyer = invoice.commercial_partner_id
# <1.4.2>
if not buyer.street and not buyer.street2:
@@ -112,11 +148,6 @@ class AccountEdiFormat(models.Model):
if not buyer.country_id:
errors.append(_("%s must have a country.", buyer.display_name))
- # <2.2.1>
- for invoice_line in invoice.invoice_line_ids:
- if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
- raise UserError(_("You must select one and only one tax by line."))
-
if any(line.quantity < 0 for line in invoice.invoice_line_ids):
errors.append(_("All quantities should be positive."))
@@ -126,6 +157,22 @@ class AccountEdiFormat(models.Model):
return errors
+ def _l10n_it_get_document_type(self, invoice):
+ errors_buyer = self._l10n_it_edi_check_buyer_invoice_configuration(invoice)
+ if errors_buyer: # Simplified invoice case
+ if invoice.move_type == 'out_invoice':
+ return 'TD07'
+ elif invoice.move_type == 'out_refund':
+ return 'TD08'
+
+ if invoice.move_type == 'out_invoice':
+ return 'TD01'
+ elif invoice.move_type == 'out_refund':
+ return 'TD04'
+
+ def _l10n_it_is_simplified_document_type(self, document_type):
+ return document_type in ['TD07', 'TD08', 'TD09']
+
# -------------------------------------------------------------------------
# Export
# -------------------------------------------------------------------------
@@ -285,14 +332,20 @@ class AccountEdiFormat(models.Model):
# TD04 == credit note
# TD05 == debit note
# TD06 == fee
+ # TD07 == simplified invoice
+ # TD08 == simplified credit note
+ # TD09 == simplified debit note
# For unsupported document types, just assume in_invoice, and log that the type is unsupported
elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento')
move_type = 'in_invoice'
- if elements and elements[0].text and elements[0].text == 'TD04':
+ document_type = elements[0].text if elements else ''
+ if document_type and document_type in ['TD04', 'TD08']:
move_type = 'in_refund'
- elif elements and elements[0].text and elements[0].text != 'TD01':
+ elif document_type and document_type not in ['TD01', 'TD07']:
_logger.info('Document type not managed: %s. Invoice type is set by default.', elements[0].text)
+ simplified = self._l10n_it_is_simplified_document_type(document_type)
+
# Setup the context for the Invoice Form
invoice_ctx = invoice.with_company(company) \
.with_context(default_move_type=move_type)
@@ -431,7 +484,11 @@ class AccountEdiFormat(models.Model):
invoice._compose_info_message(body_tree, './/DatiPagamento')))
# Invoice lines. <2.2.1>
- elements = body_tree.xpath('.//DettaglioLinee')
+ if not simplified:
+ elements = body_tree.xpath('.//DettaglioLinee')
+ else:
+ elements = body_tree.xpath('.//DatiBeniServizi')
+
if elements:
for element in elements:
with invoice_form.invoice_line_ids.new() as invoice_line_form:
@@ -470,11 +527,6 @@ class AccountEdiFormat(models.Model):
if line_elements:
invoice_line_form.name = " ".join(line_elements[0].text.split())
- # Price Unit.
- line_elements = element.xpath('.//PrezzoUnitario')
- if line_elements:
- invoice_line_form.price_unit = float(line_elements[0].text)
-
# Quantity.
line_elements = element.xpath('.//Quantita')
if line_elements:
@@ -483,11 +535,30 @@ class AccountEdiFormat(models.Model):
invoice_line_form.quantity = 1
# Taxes
- tax_element = element.xpath('.//AliquotaIVA')
+ percentage = None
+ price_subtotal = 0
+ if not simplified:
+ tax_element = element.xpath('.//AliquotaIVA')
+ if tax_element and tax_element[0].text:
+ percentage = float(tax_element[0].text)
+ else:
+ amount_element = element.xpath('.//Importo')
+ if amount_element and amount_element[0].text:
+ amount = float(amount_element[0].text)
+ tax_element = element.xpath('.//Aliquota')
+ if tax_element and tax_element[0].text:
+ percentage = float(tax_element[0].text)
+ price_subtotal = amount / (1 + percentage / 100)
+ else:
+ tax_element = element.xpath('.//Imposta')
+ if tax_element and tax_element[0].text:
+ tax_amount = float(tax_element[0].text)
+ price_subtotal = amount - tax_amount
+ percentage = round(tax_amount / price_subtotal * 100)
+
natura_element = element.xpath('.//Natura')
invoice_line_form.tax_ids.clear()
- if tax_element and tax_element[0].text:
- percentage = float(tax_element[0].text)
+ if percentage is not None:
if natura_element and natura_element[0].text:
l10n_it_kind_exoneration = natura_element[0].text
tax = self.env['account.tax'].search([
@@ -519,6 +590,14 @@ class AccountEdiFormat(models.Model):
percentage,
invoice_line_form.name))
+ # Price Unit.
+ if not simplified:
+ line_elements = element.xpath('.//PrezzoUnitario')
+ if line_elements:
+ invoice_line_form.price_unit = float(line_elements[0].text)
+ else:
+ invoice_line_form.price_unit = price_subtotal
+
# Discounts
discount_elements = element.xpath('.//ScontoMaggiorazione')
if discount_elements:
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index 6f9412a513a..6bcdaeafa06 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -147,12 +147,9 @@ class AccountMove(models.Model):
formato_trasmissione = "FPA12" if self._is_commercial_partner_pa() else "FPR12"
- if self.move_type == 'out_invoice':
- document_type = 'TD01'
- elif self.move_type == 'out_refund':
- document_type = 'TD04'
- else:
- document_type = 'TD0X'
+ document_type = self.env['account.edi.format']._l10n_it_get_document_type(self)
+ if self.env['account.edi.format']._l10n_it_is_simplified_document_type(document_type):
+ formato_trasmissione = "FSM10"
# b64encode returns a bytestring, the template tries to turn it to string,
# but only gets the repr(pdf) --> "b''"
@@ -202,7 +199,12 @@ class AccountMove(models.Model):
:return: The XML content as str.
'''
template_values = self._prepare_fatturapa_export_values()
- content = self.env['ir.qweb']._render('l10n_it_edi.account_invoice_it_FatturaPA_export', template_values)
+ if not self.env['account.edi.format']._l10n_it_is_simplified_document_type(template_values['document_type']):
+ content = self.env['ir.qweb']._render('l10n_it_edi.account_invoice_it_FatturaPA_export', template_values)
+ else:
+ content = self.env['ir.qweb']._render('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', template_values)
+ self.message_post(body=_("A simplified invoice was created instead of an ordinary one. This is because the invoice \
+ is a domestic invoice with a total amount of less than or equal to 400€ and the customer's address is incomplete."))
return content
def _post(self, soft=True):
diff --git a/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_simpl.xml b/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_simpl.xml
new file mode 100644
index 00000000000..806871032f0
--- /dev/null
+++ b/addons/l10n_it_edi_sdicoop/tests/expected_xmls/IT00470550013_simpl.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ IT
+ 01234560157
+
+ ___ignore___
+ FSM10
+ 0000000
+
+
+
+ IT
+ 01234560157
+
+ 01234560157
+ company_2_data
+
+ 1234 Test Street
+ 12345
+ Prova
+ IT
+
+ RF01
+
+
+
+
+
+
+
+ TD07
+ EUR
+ 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
index f70f7ff57a0..5a87ac1fbb6 100644
--- a/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py
+++ b/addons/l10n_it_edi_sdicoop/tests/test_edi_xml.py
@@ -9,6 +9,7 @@ from freezegun import freeze_time
from odoo import tools
from odoo.tests import tagged
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -58,6 +59,25 @@ class TestItEdi(AccountEdiTestCommon):
'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 = {
'name': 'standard_line',
'quantity': 1,
@@ -65,6 +85,20 @@ class TestItEdi(AccountEdiTestCommon):
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
}
+ 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),
@@ -146,6 +180,56 @@ class TestItEdi(AccountEdiTestCommon):
],
})
+ 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),
+ '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),
+ '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),
+ '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),
+ 'partner_id': cls.american_partner.id,
+ 'invoice_line_ids': [
+ (0, 0, {
+ **cls.standard_line_below_400,
+ }),
+ ],
+ })
+
# 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',
@@ -160,8 +244,11 @@ class TestItEdi(AccountEdiTestCommon):
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.edi_basis_xml = cls._get_test_file_content('IT00470550013_basis.xml')
+ cls.edi_simplified_basis_xml = cls._get_test_file_content('IT00470550013_simpl.xml')
@classmethod
def _get_test_file_content(cls, filename):
@@ -170,7 +257,6 @@ class TestItEdi(AccountEdiTestCommon):
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
@@ -247,7 +333,6 @@ class TestItEdi(AccountEdiTestCommon):
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
@@ -305,7 +390,6 @@ class TestItEdi(AccountEdiTestCommon):
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(
@@ -340,7 +424,6 @@ class TestItEdi(AccountEdiTestCommon):
invoice_etree = self.with_applied_xpath(invoice_etree, "")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
- @freeze_time('2020-03-24')
def test_non_latin_and_latin_inovice(self):
invoice_etree = etree.fromstring(self.non_latin_and_latin_invoice._export_as_xml())
expected_etree = self.with_applied_xpath(
@@ -386,3 +469,67 @@ class TestItEdi(AccountEdiTestCommon):
''')
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.below_400_codice_simplified_invoice._export_as_xml())
+ 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.total_400_VAT_simplified_invoice._export_as_xml())
+ 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()