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"]