From 2aaafcd7a8309bf4356f5f81593a938ceb6f8b01 Mon Sep 17 00:00:00 2001 From: Patrick T Date: Thu, 3 Aug 2023 11:07:35 +0200 Subject: [PATCH] [IMP] l10n_it_fatturapa_in: Update logic to import e-invoice xml Update logic to import e-invoice xml, porting logic from PR #3100 and adapting it to v. 14.0 --- l10n_it_fatturapa_in/models/attachment.py | 21 +- .../tests/data/IT03309970733_VATG1.xml | 108 ++++++++ .../tests/data/IT03309970733_VATG2.xml | 141 +++++++++++ .../tests/test_import_fatturapa_xml.py | 96 +++++++ .../wizard/wizard_import_fatturapa.py | 238 +++++++++++------- 5 files changed, 507 insertions(+), 97 deletions(-) create mode 100644 l10n_it_fatturapa_in/tests/data/IT03309970733_VATG1.xml create mode 100644 l10n_it_fatturapa_in/tests/data/IT03309970733_VATG2.xml diff --git a/l10n_it_fatturapa_in/models/attachment.py b/l10n_it_fatturapa_in/models/attachment.py index d294daaa9669..423ffe59722b 100644 --- a/l10n_it_fatturapa_in/models/attachment.py +++ b/l10n_it_fatturapa_in/models/attachment.py @@ -137,16 +137,17 @@ def extract_attachments(self, AttachmentsData, invoice_id): name = _("Attachment without name") else: name = attach.NomeAttachment - content = attach.Attachment.encode() - _attach_dict = { - "name": name, - "datas": content, - "description": attach.DescrizioneAttachment or "", - "compression": attach.AlgoritmoCompressione or "", - "format": attach.FormatoAttachment or "", - "invoice_id": invoice_id, - } - AttachModel.create(_attach_dict) + if attach.Attachment: + content = attach.Attachment.encode() + _attach_dict = { + "name": name, + "datas": content, + "description": attach.DescrizioneAttachment or "", + "compression": attach.AlgoritmoCompressione or "", + "format": attach.FormatoAttachment or "", + "invoice_id": invoice_id, + } + AttachModel.create(_attach_dict) @api.depends("ir_attachment_id.datas") def _compute_linked_invoice_id_xml(self): diff --git a/l10n_it_fatturapa_in/tests/data/IT03309970733_VATG1.xml b/l10n_it_fatturapa_in/tests/data/IT03309970733_VATG1.xml new file mode 100644 index 000000000000..5a542ac3c580 --- /dev/null +++ b/l10n_it_fatturapa_in/tests/data/IT03309970733_VATG1.xml @@ -0,0 +1,108 @@ + + + + + + IT + 05979361218 + + VATG1 + FPA12 + UFPQ1O + + + + + IT + 03309970733 + + MRORSS90E25B111T + + SOCIETA' ALPHA BETA SRL + + RF02 + + + VIALE ROMA 543B + 07100 + SASSARI + SS + IT + + + + + 80213330584 + + AMMINISTRAZIONE BETA + + + + VIA TORINO 38-B + 00145 + ROMA + RM + IT + + + + + + + TD01 + EUR + 2015-02-16 + FT/2015/0009 + Rif ordine MAPA: --- Nr. Identificativo Ordine 1234567 + + + + + 1 + + SA + 123456-01 + + USB + 4.00 + PZ + 177.00 + + SC + 10.00 + + 637.20 + 22.00 + D122353 + + + 2 + + SA + 123456-04 + + USB + 1.00 + PZ + 596.00 + + SC + 10.00 + + 536.40 + 22.00 + D122354 + + + 22.00 + 1173.60 + 258.19 + S + SCISSIONE PAGAMENTI Split Payment art.17-ter del DPR 633/1972 + + + + diff --git a/l10n_it_fatturapa_in/tests/data/IT03309970733_VATG2.xml b/l10n_it_fatturapa_in/tests/data/IT03309970733_VATG2.xml new file mode 100644 index 000000000000..663f8eb015fd --- /dev/null +++ b/l10n_it_fatturapa_in/tests/data/IT03309970733_VATG2.xml @@ -0,0 +1,141 @@ + + + + + + IT + 05979361218 + + VATG2 + FPA12 + UFPQ1O + + + + + IT + 03309970733 + + 03533590174 + + SOCIETA' ALPHA BETA SRL + + RF02 + + + VIALE ROMA 543B + 07100 + SASSARI + SS + IT + + + + + + IT + 03309970733 + + + Rappresentante fiscale + + + + + + 80213330584 + + AMMINISTRAZIONE BETA + + + + VIA TORINO 38-B + 00145 + ROMA + RM + IT + + + + + + IT + 03309970733 + + + Terzo Intermediario + + + + + + + + TD01 + EUR + 2015-02-16 + FT/2015/0009 + Rif ordine MAPA: --- Nr. Identificativo Ordine 1234567 + + + + + IT + 03309970733 + + + Trasporto spa + + + + + + + 1 + + SA + 123456-01 + + USB + 4.00 + PZ + 177.00 + + SC + 10.00 + + 637.20 + 22.00 + D122353 + + + 2 + + SA + 123456-04 + + USB + 1.00 + PZ + 596.00 + + SC + 10.00 + + 536.40 + 22.00 + D122354 + + + 22.00 + 1173.60 + 258.19 + S + SCISSIONE PAGAMENTI Split Payment art.17-ter del DPR 633/1972 + + + + diff --git a/l10n_it_fatturapa_in/tests/test_import_fatturapa_xml.py b/l10n_it_fatturapa_in/tests/test_import_fatturapa_xml.py index a5e37d75f9cd..4b2e55194b51 100644 --- a/l10n_it_fatturapa_in/tests/test_import_fatturapa_xml.py +++ b/l10n_it_fatturapa_in/tests/test_import_fatturapa_xml.py @@ -969,6 +969,102 @@ def vat_partner_exists(): ), ) + def test_54_duplicated_partner(self): + """If there are multiple partners with the same VAT + and we try to import an Electronic Invoice for that VAT, + an exception is raised.""" + # Arrange: There are two partners with the same VAT + common_vat = "IT03309970733" + partners = self.env["res.partner"].create( + [ + { + "name": "Test partner1", + "vat": common_vat, + }, + { + "name": "Test partner2", + "vat": common_vat, + }, + ] + ) + + # Update any conflicting partner from other tests + existing_partners = self.env["res.partner"].search( + [ + ("sanitized_vat", "=", common_vat), + ("id", "not in", partners.ids), + ], + ) + existing_partners.update( + { + "vat": "IT12345670017", + } + ) + + # Assert: The import wizard can't choose between the two created partners + with self.assertRaises(UserError) as ue: + self.run_wizard("VATG1", "IT03309970733_VATG1.xml") + exc_message = ue.exception.args[0] + self.assertIn("Two distinct partners", exc_message) + self.assertIn("VAT number", exc_message) + for partner in partners: + self.assertIn(partner.name, exc_message) + + def test_55_xml_import_vat_group(self): + """Importing bills from VAT groups creates different suppliers.""" + # Arrange: The involved XMLs contain suppliers from a VAT group: + # the suppliers have the same VAT `common_vat`, + # but each supplier has a different fiscal code + common_vat = "IT03309970733" + vat_group_1_fiscalcode = "MRORSS90E25B111T" + vat_group_2_fiscalcode = "03533590174" + + # Update any conflicting partner from other tests + existing_partners = self.env["res.partner"].search( + [ + "|", + ("sanitized_vat", "=", common_vat), + ( + "fiscalcode", + "in", + ( + vat_group_1_fiscalcode, + vat_group_2_fiscalcode, + ), + ), + ], + ) + existing_partners.update( + { + "vat": "IT12345670017", + "fiscalcode": "1234567890123456", + } + ) + + # Act: Import the XMLs, + # checking that the suppliers match the data in the XML + res = self.run_wizard("VATG1", "IT03309970733_VATG1.xml") + invoice_model = res.get("res_model") + invoice_domain = res.get("domain") + invoice_vat_group_1 = self.env[invoice_model].search(invoice_domain) + vat_group_1_partner = invoice_vat_group_1.partner_id + self.assertEqual(vat_group_1_partner.vat, common_vat) + self.assertEqual(vat_group_1_partner.fiscalcode, vat_group_1_fiscalcode) + + res = self.run_wizard("VATG2", "IT03309970733_VATG2.xml") + invoice_model = res.get("res_model") + invoice_domain = res.get("domain") + invoice_vat_group_2 = self.env[invoice_model].search(invoice_domain) + vat_group_2_partner = invoice_vat_group_2.partner_id + self.assertEqual(vat_group_2_partner.vat, common_vat) + self.assertEqual(vat_group_2_partner.fiscalcode, vat_group_2_fiscalcode) + + # Assert: Two different partners have been created + self.assertNotEqual( + vat_group_1_partner, + vat_group_2_partner, + ) + def test_01_xml_link(self): """ E-invoice lines are created. diff --git a/l10n_it_fatturapa_in/wizard/wizard_import_fatturapa.py b/l10n_it_fatturapa_in/wizard/wizard_import_fatturapa.py index e944d19c99dd..6c5264d28faf 100644 --- a/l10n_it_fatturapa_in/wizard/wizard_import_fatturapa.py +++ b/l10n_it_fatturapa_in/wizard/wizard_import_fatturapa.py @@ -8,7 +8,9 @@ from odoo import api, fields, models, registry from odoo.exceptions import UserError from odoo.fields import first +from odoo.osv import expression from odoo.tools import float_is_zero, frozendict +from odoo.tools.safe_eval import safe_eval from odoo.tools.translate import _ from odoo.addons.base_iban.models.res_partner_bank import pretty_iban @@ -27,6 +29,10 @@ } +class PartnerDuplicatedException(UserError): + pass + + class WizardImportFatturapa(models.TransientModel): _name = "wizard.import.fatturapa" _description = "Import E-bill" @@ -182,110 +188,168 @@ def check_partner_base_data(self, partner_id, DatiAnagrafici): % (DatiAnagrafici.Anagrafica.Cognome, partner.lastname) ) - def getPartnerBase(self, DatiAnagrafici): # noqa: C901 - if not DatiAnagrafici: - return False - partner_model = self.env["res.partner"] - cf = DatiAnagrafici.CodiceFiscale or False - vat = False - if DatiAnagrafici.IdFiscaleIVA: - id_paese = DatiAnagrafici.IdFiscaleIVA.IdPaese.upper() - id_codice = re.sub(r"\W+", "", DatiAnagrafici.IdFiscaleIVA.IdCodice).upper() - # Format Italian VAT ID to always have 11 char - # to avoid validation error when creating the given partner - if id_paese == "IT" and not id_codice.startswith("IT"): - vat = "IT{}".format(id_codice.rjust(11, "0")[:11]) - # XXX maybe San Marino needs special formatting too? - else: - vat = id_codice - partners = partner_model + def _get_partner_domains_by_vat_fc(self, vat, fc): + """Return domains for searching partner using VAT and FC. + Search by VAT first, then by FC. + Security rule `res.partner company` is used if it is enabled. + """ + vat_domain = [("vat", "=", vat)] + fc_domain = [("fiscalcode", "=", fc)] + domains = list() + if vat and fc: + # The partner must match exactly (both VAT and FC) + domains.append(expression.AND([vat_domain, fc_domain])) + # Or it is missing either FC or VAT + no_vat_domain = [("vat", "=", False)] + no_fc_domain = [("fiscalcode", "=", False)] + vat_domain = expression.AND([vat_domain, no_fc_domain]) + fc_domain = expression.AND([no_vat_domain, fc_domain]) + + if vat: + domains.append(vat_domain) + if fc: + domains.append(fc_domain) + + # Inject the multi-company partners sharing rule, if enabled res_partner_rule = ( self.env["ir.model.data"] .sudo() .xmlid_to_object("base.res_partner_rule", raise_if_not_found=False) ) - if vat: - domain = [("vat", "=", vat)] - if ( - self.env.context.get("from_attachment") - and res_partner_rule - and res_partner_rule.active - ): - att = self.env.context.get("from_attachment") - domain.extend( - [ - "|", - ("company_id", "child_of", att.company_id.id), - ("company_id", "=", False), - ] - ) - partners = partner_model.search(domain) - if not partners and cf: - domain = [("fiscalcode", "=", cf)] - if ( - self.env.context.get("from_attachment") - and res_partner_rule - and res_partner_rule.active - ): - att = self.env.context.get("from_attachment") - domain.extend( + att = self.env.context.get("from_attachment") + if att and res_partner_rule and res_partner_rule.active: + partner_rule_domain = res_partner_rule.domain_force + partner_rule_domain = partner_rule_domain.replace( + "user.company_id", + "company_id", + ) + partner_rule_domain = safe_eval( + partner_rule_domain, + locals_dict={ + **res_partner_rule._eval_context(), + "company_ids": att.company_id.ids, + "company_id": att.company_id, + }, + ) + for domain_index, domain in enumerate(domains): + domain = domains[domain_index] + domains[domain_index] = expression.AND( [ - "|", - ("company_id", "child_of", att.company_id.id), - ("company_id", "=", False), + domain, + partner_rule_domain, ] ) + return domains + + def _search_partner_by_vat_fc(self, vat, fc): + """Search partner using VAT and FC.""" + domains = self._get_partner_domains_by_vat_fc(vat, fc) + partner_model = self.env["res.partner"] + for domain in domains: partners = partner_model.search(domain) - commercial_partner_id = False + if partners: + break + else: + partners = partner_model.browse() + return partners + + def _get_commercial_partner(self, partners): + """Get the common commercial partner from `partners`.""" if len(partners) > 1: + # Ensure that all found partners have the same commercial partner + commercial_partner = self.env["res.partner"].browse() for partner in partners: + partner_commercial_partner = partner.commercial_partner_id if ( - commercial_partner_id - and partner.commercial_partner_id.id != commercial_partner_id + commercial_partner + and partner_commercial_partner != commercial_partner ): - self.log_inconsistency( + same_vat_cf_partners = ( + partner_commercial_partner | commercial_partner + ) + raise PartnerDuplicatedException( _( - "Two distinct partners with " - "VAT number %s or Fiscal Code %s already " - "present in db." % (vat, cf) + "Two distinct partners {partners} with " + "VAT number {vat} or Fiscal Code {cf} already " + "present in db." + ).format( + partners=", ".join( + same_vat_cf_partners.mapped("display_name") + ), + vat=partner.vat, + cf=partner.fiscalcode, ) ) - return False - commercial_partner_id = partner.commercial_partner_id.id - if partners: - if not commercial_partner_id: - commercial_partner_id = partners[0].commercial_partner_id.id - self.check_partner_base_data(commercial_partner_id, DatiAnagrafici) - return commercial_partner_id + commercial_partner = partner_commercial_partner + elif len(partners) == 1: + commercial_partner = first(partners).commercial_partner_id else: - # partner to be created - country_id = False - if DatiAnagrafici.IdFiscaleIVA: - CountryCode = DatiAnagrafici.IdFiscaleIVA.IdPaese - countries = self.CountryByCode(CountryCode) - if countries: - country_id = countries[0].id - else: - raise UserError( - _("Country Code %s not found in system.") % CountryCode - ) - vals = { - "vat": vat, - "fiscalcode": cf, - "is_company": ( - DatiAnagrafici.Anagrafica.Denominazione and True or False - ), - "eori_code": DatiAnagrafici.Anagrafica.CodEORI or "", - "country_id": country_id, - } - if DatiAnagrafici.Anagrafica.Nome: - vals["firstname"] = DatiAnagrafici.Anagrafica.Nome - if DatiAnagrafici.Anagrafica.Cognome: - vals["lastname"] = DatiAnagrafici.Anagrafica.Cognome - if DatiAnagrafici.Anagrafica.Denominazione: - vals["name"] = DatiAnagrafici.Anagrafica.Denominazione - - return partner_model.create(vals).id + commercial_partner = self.env["res.partner"].browse() + return commercial_partner + + def _extract_vat(self, DatiAnagrafici): + """Extract VAT from node DatiAnagrafici.""" + vat = False + if DatiAnagrafici.IdFiscaleIVA: + id_paese = DatiAnagrafici.IdFiscaleIVA.IdPaese.upper() + id_codice = re.sub(r"\W+", "", DatiAnagrafici.IdFiscaleIVA.IdCodice).upper() + # Format Italian VAT ID to always have 11 char + # to avoid validation error when creating the given partner + if id_paese == "IT" and not id_codice.startswith("IT"): + vat = "IT{}".format(id_codice.rjust(11, "0")[:11]) + # XXX maybe San Marino needs special formatting too? + else: + vat = id_codice + return vat + + def _prepare_partner_values(self, DatiAnagrafici, cf, vat): + country_id = False + if DatiAnagrafici.IdFiscaleIVA: + CountryCode = DatiAnagrafici.IdFiscaleIVA.IdPaese + countries = self.CountryByCode(CountryCode) + if countries: + country_id = countries[0].id + else: + raise UserError(_("Country Code %s not found in system.") % CountryCode) + vals = { + "vat": vat, + "fiscalcode": cf, + "is_company": (DatiAnagrafici.Anagrafica.Denominazione and True or False), + "eori_code": DatiAnagrafici.Anagrafica.CodEORI or "", + "country_id": country_id, + } + if DatiAnagrafici.Anagrafica.Nome: + vals["firstname"] = DatiAnagrafici.Anagrafica.Nome + if DatiAnagrafici.Anagrafica.Cognome: + vals["lastname"] = DatiAnagrafici.Anagrafica.Cognome + if DatiAnagrafici.Anagrafica.Denominazione: + vals["name"] = DatiAnagrafici.Anagrafica.Denominazione + return vals + + def getPartnerBase(self, DatiAnagrafici, raise_if_duplicated=True): + if not DatiAnagrafici: + return False + cf = DatiAnagrafici.CodiceFiscale or False + vat = self._extract_vat(DatiAnagrafici) + try: + partners = self._search_partner_by_vat_fc(vat, cf) + commercial_partner = self._get_commercial_partner(partners) + except PartnerDuplicatedException as duplicated_exception: + if raise_if_duplicated: + raise duplicated_exception + else: + self.log_inconsistency(duplicated_exception.args[0]) + found_partner = self.env["res.partner"].browse() + else: + if commercial_partner: + commercial_partner_id = commercial_partner.id + self.check_partner_base_data(commercial_partner_id, DatiAnagrafici) + found_partner = commercial_partner + else: + # partner to be created + vals = self._prepare_partner_values(DatiAnagrafici, cf, vat) + found_partner = self.env["res.partner"].create(vals) + return found_partner.id def getCedPrest(self, cedPrest): partner_model = self.env["res.partner"]