odoo-addon-l10n-it-edi-extension 18.0.1.0.0.30__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. odoo/addons/l10n_it_edi_extension/README.rst +493 -0
  2. odoo/addons/l10n_it_edi_extension/__init__.py +951 -0
  3. odoo/addons/l10n_it_edi_extension/__manifest__.py +37 -0
  4. odoo/addons/l10n_it_edi_extension/controllers/__init__.py +1 -0
  5. odoo/addons/l10n_it_edi_extension/controllers/portal.py +27 -0
  6. odoo/addons/l10n_it_edi_extension/data/FoglioStileAssoSoftware.xsl +3150 -0
  7. odoo/addons/l10n_it_edi_extension/data/invoice_it_template.xml +50 -0
  8. odoo/addons/l10n_it_edi_extension/data/res.city.it.code.csv +13898 -0
  9. odoo/addons/l10n_it_edi_extension/i18n/l10n_it_edi_extension.pot +1167 -0
  10. odoo/addons/l10n_it_edi_extension/i18n/l10n_it_edi_fatturapa.pot +44 -0
  11. odoo/addons/l10n_it_edi_extension/models/__init__.py +15 -0
  12. odoo/addons/l10n_it_edi_extension/models/account_journal.py +152 -0
  13. odoo/addons/l10n_it_edi_extension/models/account_move.py +765 -0
  14. odoo/addons/l10n_it_edi_extension/models/account_move_line.py +10 -0
  15. odoo/addons/l10n_it_edi_extension/models/ir_attachment.py +37 -0
  16. odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_activity_progress.py +14 -0
  17. odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_article_code.py +15 -0
  18. odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_discount_rise_price.py +25 -0
  19. odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_line.py +48 -0
  20. odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_line_other_data.py +17 -0
  21. odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_summary_data.py +77 -0
  22. odoo/addons/l10n_it_edi_extension/models/res_city_it_code.py +83 -0
  23. odoo/addons/l10n_it_edi_extension/models/res_company.py +62 -0
  24. odoo/addons/l10n_it_edi_extension/models/res_partner.py +64 -0
  25. odoo/addons/l10n_it_edi_extension/readme/CONFIGURE.md +45 -0
  26. odoo/addons/l10n_it_edi_extension/readme/CONTRIBUTORS.md +5 -0
  27. odoo/addons/l10n_it_edi_extension/readme/DESCRIPTION.md +160 -0
  28. odoo/addons/l10n_it_edi_extension/security/ir.model.access.csv +14 -0
  29. odoo/addons/l10n_it_edi_extension/static/description/icon.png +0 -0
  30. odoo/addons/l10n_it_edi_extension/static/description/index.html +832 -0
  31. odoo/addons/l10n_it_edi_extension/tests/__init__.py +2 -0
  32. odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT01234567890_FPR03.xml +166 -0
  33. odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT02780790107_11004.xml +216 -0
  34. odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT02780790107_11005.xml +224 -0
  35. odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT05979361218_003.xml +107 -0
  36. odoo/addons/l10n_it_edi_extension/tests/import_xmls/test.png +0 -0
  37. odoo/addons/l10n_it_edi_extension/tests/import_xmls/xml_import.zip +0 -0
  38. odoo/addons/l10n_it_edi_extension/tests/test_fiscalcode.py +126 -0
  39. odoo/addons/l10n_it_edi_extension/tests/test_import_edi_extension_xml.py +350 -0
  40. odoo/addons/l10n_it_edi_extension/views/company_view.xml +41 -0
  41. odoo/addons/l10n_it_edi_extension/views/l10n_it_view.xml +164 -0
  42. odoo/addons/l10n_it_edi_extension/views/res_partner_view.xml +19 -0
  43. odoo/addons/l10n_it_edi_extension/wizards/__init__.py +2 -0
  44. odoo/addons/l10n_it_edi_extension/wizards/compute_fc.py +176 -0
  45. odoo/addons/l10n_it_edi_extension/wizards/compute_fc_view.xml +65 -0
  46. odoo/addons/l10n_it_edi_extension/wizards/l10n_it_edi_import_file_wizard.py +98 -0
  47. odoo/addons/l10n_it_edi_extension/wizards/l10n_it_edi_import_file_wizard.xml +46 -0
  48. odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/METADATA +513 -0
  49. odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/RECORD +51 -0
  50. odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/WHEEL +5 -0
  51. odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/top_level.txt +1 -0
@@ -0,0 +1,765 @@
1
+ # Copyright 2025 Giuseppe Borruso - Dinamiche Aziendali srl
2
+ # Copyright 2025 Simone Rubino
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
4
+
5
+ from odoo import api, fields, models
6
+ from odoo.exceptions import UserError
7
+ from odoo.tools import float_compare, html2plaintext
8
+
9
+ from odoo.addons.base.models.ir_qweb_fields import Markup
10
+ from odoo.addons.l10n_it_edi.models.account_move import get_date, get_float, get_text
11
+
12
+
13
+ class AccountMoveInherit(models.Model):
14
+ _inherit = "account.move"
15
+
16
+ l10n_it_edi_protocol_number = fields.Char(size=64, copy=False)
17
+ l10n_it_edi_tax_representative_id = fields.Many2one(
18
+ "res.partner", string="Tax Representative"
19
+ )
20
+ l10n_it_edi_sender = fields.Selection(
21
+ [("CC", "Assignee / Partner"), ("TZ", "Third Person")], string="Sender"
22
+ )
23
+ l10n_it_edi_attachment_preview_link = fields.Char(
24
+ string="Preview link",
25
+ compute="_compute_l10n_it_edi_attachment_preview_link",
26
+ )
27
+ l10n_it_edi_line_ids = fields.One2many(
28
+ "l10n_it_edi.line",
29
+ "invoice_id",
30
+ string="E-Invoice Lines",
31
+ readonly=True,
32
+ copy=False,
33
+ )
34
+ l10n_it_edi_summary_ids = fields.One2many(
35
+ "l10n_it_edi.summary_data",
36
+ "invoice_id",
37
+ string="E-Invoice Summary Data",
38
+ copy=False,
39
+ )
40
+ l10n_it_edi_activity_progress_ids = fields.One2many(
41
+ "l10n_it_edi.activity_progress",
42
+ "invoice_id",
43
+ string="E-Invoice Activity Progress",
44
+ copy=False,
45
+ )
46
+ l10n_it_edi_rounding = fields.Float(
47
+ string="Rounding",
48
+ readonly=True,
49
+ help="Possible total amount rounding on the document (negative sign allowed)",
50
+ copy=False,
51
+ )
52
+ l10n_edi_it_art73 = fields.Boolean(
53
+ string="Art. 73",
54
+ readonly=True,
55
+ help="Indicates whether the document has been issued according to "
56
+ "methods and terms laid down in a ministerial decree under the "
57
+ "terms of Article 73 of Italian Presidential Decree 633/72 (this "
58
+ "enables the seller/provider to issue in the same year several "
59
+ "documents with same number)",
60
+ copy=False,
61
+ )
62
+ l10n_it_edi_related_invoice_code = fields.Char(
63
+ string="Related Invoice Code", copy=False
64
+ )
65
+ l10n_it_edi_related_invoice_date = fields.Date(
66
+ string="Related Invoice Date", copy=False
67
+ )
68
+ l10n_it_edi_stabile_organizzazione_indirizzo = fields.Char(
69
+ string="Organization Address",
70
+ help="The fields must be entered only when the seller/provider is "
71
+ "non-resident, with a stable organization in Italy. Address of "
72
+ "the stable organization in Italy (street name, square, etc.)",
73
+ readonly=True,
74
+ copy=False,
75
+ )
76
+ l10n_it_edi_stabile_organizzazione_civico = fields.Char(
77
+ string="Organization Street Number",
78
+ help="Street number of the address (no need to specify if already "
79
+ "present in the address field)",
80
+ readonly=True,
81
+ copy=False,
82
+ )
83
+ l10n_it_edi_stabile_organizzazione_cap = fields.Char(
84
+ string="Organization ZIP", help="ZIP Code", readonly=True, copy=False
85
+ )
86
+ l10n_it_edi_stabile_organizzazione_comune = fields.Char(
87
+ string="Organization Municipality",
88
+ help="Municipality or city to which the Stable Organization refers",
89
+ readonly=True,
90
+ copy=False,
91
+ )
92
+ l10n_it_edi_stabile_organizzazione_provincia = fields.Char(
93
+ string="Organization Province",
94
+ help="Acronym of the Province to which the municipality indicated "
95
+ "in the information element 1.2.3.4 <Comune> belongs. "
96
+ "Must be filled if the information element 1.2.3.6 <Nazione> is "
97
+ "equal to IT",
98
+ readonly=True,
99
+ copy=False,
100
+ )
101
+ l10n_it_edi_stabile_organizzazione_nazione = fields.Char(
102
+ string="Organization Country",
103
+ help="Country code according to the ISO 3166-1 alpha-2 code standard",
104
+ readonly=True,
105
+ copy=False,
106
+ )
107
+ l10n_it_edi_amount_untaxed = fields.Monetary(
108
+ string="E-Invoice Untaxed Amount", readonly=True
109
+ )
110
+ l10n_it_edi_amount_tax = fields.Monetary(
111
+ string="E-Invoice Tax Amount", readonly=True
112
+ )
113
+ l10n_it_edi_amount_total = fields.Monetary(
114
+ string="E-Invoice Total Amount",
115
+ compute="_compute_l10n_it_amount_total",
116
+ readonly=True,
117
+ )
118
+ l10n_it_edi_validation_message = fields.Text(
119
+ compute="_compute_l10n_it_edi_validation_message"
120
+ )
121
+
122
+ # -------------------------------------------------------------------------
123
+ # Computes
124
+ # -------------------------------------------------------------------------
125
+
126
+ @api.depends("l10n_it_edi_attachment_id")
127
+ def _compute_l10n_it_edi_attachment_preview_link(self):
128
+ for move in self:
129
+ if move.l10n_it_edi_attachment_id:
130
+ move.l10n_it_edi_attachment_preview_link = (
131
+ move.get_base_url()
132
+ + f"/fatturapa/preview/{move.l10n_it_edi_attachment_id.id}"
133
+ )
134
+ else:
135
+ move.l10n_it_edi_attachment_preview_link = ""
136
+
137
+ @api.depends(
138
+ "l10n_it_edi_amount_untaxed", "l10n_it_edi_amount_tax", "l10n_it_edi_rounding"
139
+ )
140
+ def _compute_l10n_it_amount_total(self):
141
+ for move in self:
142
+ move.l10n_it_edi_amount_total = sum(
143
+ [
144
+ move.l10n_it_edi_amount_untaxed,
145
+ move.l10n_it_edi_amount_tax,
146
+ move.l10n_it_edi_rounding,
147
+ ]
148
+ )
149
+
150
+ @api.depends(
151
+ "move_type",
152
+ "state",
153
+ "amount_untaxed",
154
+ "amount_tax",
155
+ "amount_total",
156
+ "l10n_it_edi_attachment_id",
157
+ "l10n_it_edi_amount_untaxed",
158
+ "l10n_it_edi_amount_tax",
159
+ "l10n_it_edi_rounding",
160
+ )
161
+ def _compute_l10n_it_edi_validation_message(self):
162
+ self.l10n_it_edi_validation_message = ""
163
+
164
+ invoices_to_check = self.filtered(
165
+ lambda inv: inv.is_purchase_document()
166
+ and inv.state in ["draft", "posted"]
167
+ and inv.l10n_it_edi_attachment_id
168
+ )
169
+ for invoice in invoices_to_check:
170
+ error_messages = list()
171
+
172
+ if error_message := invoice._l10n_it_edi_check_amount_untaxed():
173
+ error_messages.append(error_message)
174
+
175
+ if error_message := invoice._l10n_it_edi_check_amount_tax():
176
+ error_messages.append(error_message)
177
+
178
+ if error_message := invoice._l10n_it_edi_check_amount_total():
179
+ error_messages.append(error_message)
180
+
181
+ if not error_messages:
182
+ continue
183
+ invoice.l10n_it_edi_validation_message = ",\n".join(error_messages) + "."
184
+
185
+ # -------------------------------------------------------------------------
186
+ # Business actions
187
+ # -------------------------------------------------------------------------
188
+
189
+ def action_l10n_it_edi_attachment_preview(self):
190
+ self.ensure_one()
191
+
192
+ return {
193
+ "type": "ir.actions.act_url",
194
+ "name": "Show preview",
195
+ "url": self.l10n_it_edi_attachment_preview_link,
196
+ "target": "new",
197
+ }
198
+
199
+ # -------------------------------------------------------------------------
200
+ # Helpers
201
+ # -------------------------------------------------------------------------
202
+
203
+ def _l10n_it_edi_add_base_lines_xml_values(
204
+ self, base_lines_aggregated_values, is_downpayment
205
+ ):
206
+ res = super()._l10n_it_edi_add_base_lines_xml_values(
207
+ base_lines_aggregated_values, is_downpayment
208
+ )
209
+ for base_line, _aggregated_values in base_lines_aggregated_values:
210
+ line = base_line["record"]
211
+ base_line["it_values"].update(
212
+ {
213
+ "admin_ref": line.l10n_it_edi_admin_ref or None,
214
+ }
215
+ )
216
+ return res
217
+
218
+ def _l10n_it_edi_get_values(self, pdf_values=None):
219
+ res = super()._l10n_it_edi_get_values(pdf_values)
220
+
221
+ causale_list = []
222
+ if self.narration:
223
+ try:
224
+ narration_text = html2plaintext(self.narration)
225
+ except Exception:
226
+ narration_text = ""
227
+
228
+ # max length of Causale is 200
229
+ for causale in narration_text.split("\n"):
230
+ if not causale:
231
+ continue
232
+ causale_list_200 = [
233
+ causale[i : i + 200] for i in range(0, len(causale), 200)
234
+ ]
235
+ for causale200 in causale_list_200:
236
+ causale_list.append(causale200)
237
+
238
+ res["causale"] = causale_list
239
+
240
+ return res
241
+
242
+ def _l10n_it_edi_get_extra_info(
243
+ self, company, document_type, body_tree, incoming=True
244
+ ):
245
+ extra_info, message_to_log = super()._l10n_it_edi_get_extra_info(
246
+ company, document_type, body_tree, incoming=incoming
247
+ )
248
+
249
+ if sender := get_text(body_tree, "//SoggettoEmittente"):
250
+ self.l10n_it_edi_sender = sender
251
+
252
+ if elements_stabile_organizzazione := body_tree.xpath(
253
+ "//StabileOrganizzazione"
254
+ ):
255
+ element_stabile_organizzazione = elements_stabile_organizzazione[0]
256
+ self.update(
257
+ {
258
+ "l10n_it_edi_stabile_organizzazione_indirizzo": get_text(
259
+ element_stabile_organizzazione, ".//Indirizzo"
260
+ ),
261
+ "l10n_it_edi_stabile_organizzazione_civico": get_date(
262
+ element_stabile_organizzazione, ".//NumeroCivico"
263
+ ),
264
+ "l10n_it_edi_stabile_organizzazione_cap": get_date(
265
+ element_stabile_organizzazione, ".//CAP"
266
+ ),
267
+ "l10n_it_edi_stabile_organizzazione_comune": get_date(
268
+ element_stabile_organizzazione, ".//Comune"
269
+ ),
270
+ "l10n_it_edi_stabile_organizzazione_provincia": get_date(
271
+ element_stabile_organizzazione, ".//Provincia"
272
+ ),
273
+ "l10n_it_edi_stabile_organizzazione_nazione": get_date(
274
+ element_stabile_organizzazione, ".//Nazione"
275
+ ),
276
+ }
277
+ )
278
+
279
+ if rounding := get_float(body_tree, ".//DatiGeneraliDocumento/Arrotondamento"):
280
+ self.l10n_it_edi_rounding = rounding
281
+
282
+ if get_text(body_tree, "//DatiGeneraliDocumento/Art73"):
283
+ self.l10n_edi_it_art73 = True
284
+
285
+ if elements_sal := body_tree.xpath(".//DatiGenerali/DatiSAL"):
286
+ self.env["l10n_it_edi.activity_progress"].create(
287
+ [
288
+ {
289
+ "activity_progress": get_text(
290
+ element_sal, ".//RiferimentoFase"
291
+ ),
292
+ "invoice_id": self.id,
293
+ }
294
+ for element_sal in elements_sal
295
+ ],
296
+ )
297
+
298
+ for xpath, label in [
299
+ (
300
+ ".//DatiGenerali/DatiTrasporto",
301
+ self.env._("Transport informations from XML file:"),
302
+ ),
303
+ (".//DatiVeicoli", self.env._("Vehicle informations from XML file:")),
304
+ ]:
305
+ if body_tree.xpath(xpath):
306
+ message = Markup("<br/>").join(
307
+ (label, self._compose_info_message(body_tree, xpath))
308
+ )
309
+ message_to_log.append(message)
310
+
311
+ if elements_parent_invoice := body_tree.xpath(
312
+ ".//DatiGenerali/FatturaPrincipale"
313
+ ):
314
+ for element_parent_invoice in elements_parent_invoice:
315
+ self.write(
316
+ {
317
+ "l10n_it_edi_related_invoice_code": get_text(
318
+ element_parent_invoice, ".//NumeroFatturaPrincipale"
319
+ ),
320
+ "l10n_it_edi_related_invoice_date": get_date(
321
+ element_parent_invoice, ".//DataFatturaPrincipale"
322
+ ),
323
+ }
324
+ )
325
+
326
+ tag_name = (
327
+ ".//DettaglioLinee"
328
+ if not extra_info["simplified"]
329
+ else ".//DatiBeniServizi"
330
+ )
331
+ if elements_line := body_tree.xpath(tag_name):
332
+ for element_line in elements_line:
333
+ self.l10n_it_edi_amount_untaxed += get_float(
334
+ element_line, ".//PrezzoTotale"
335
+ )
336
+
337
+ if elements_summary := body_tree.xpath(".//DatiBeniServizi/DatiRiepilogo"):
338
+ self.env["l10n_it_edi.summary_data"].create(
339
+ [
340
+ {
341
+ "tax_rate": get_float(element_summary, ".//AliquotaIVA"),
342
+ "non_taxable_nature": get_text(element_summary, ".//Natura"),
343
+ "incidental_charges": get_float(
344
+ element_summary, ".//SpeseAccessorie"
345
+ ),
346
+ "rounding": get_float(element_summary, ".//Arrotondamento"),
347
+ "amount_untaxed": get_float(
348
+ element_summary, ".//ImponibileImporto"
349
+ ),
350
+ "amount_tax": get_float(element_summary, ".//Imposta"),
351
+ "payability": get_text(element_summary, ".//EsigibilitaIVA"),
352
+ "law_reference": get_text(
353
+ element_summary, ".//RiferimentoNormativo"
354
+ ),
355
+ "invoice_id": self.id,
356
+ }
357
+ for element_summary in elements_summary
358
+ ]
359
+ )
360
+ for element_summary in elements_summary:
361
+ self.l10n_it_edi_amount_tax += get_float(element_summary, ".//Imposta")
362
+
363
+ extra_info["l10n_it_edi_ext_body_tree"] = body_tree
364
+ return extra_info, message_to_log
365
+
366
+ def _l10n_it_edi_update_partner(self, xml_tree, role, partner):
367
+ vals = self._l10n_it_edi_extension_prepare_partner_values(xml_tree, role)
368
+ partner.update(vals)
369
+ return partner
370
+
371
+ def _l10n_it_edi_ext_import_summary_line(self, element, extra_info=None):
372
+ messages_to_log = []
373
+ company = self.company_id
374
+ percentage = get_float(element, ".//AliquotaIVA")
375
+ extra_domain = extra_info.get(
376
+ "type_tax_use_domain", [("type_tax_use", "=", "purchase")]
377
+ )
378
+ l10n_it_exempt_reason = get_text(element, ".//Natura").upper() or False
379
+ tax = self._l10n_it_edi_search_tax_for_import(
380
+ company,
381
+ percentage,
382
+ extra_domain,
383
+ l10n_it_exempt_reason=l10n_it_exempt_reason,
384
+ )
385
+ if tax:
386
+ self.invoice_line_ids += self.env["account.move.line"].create(
387
+ {
388
+ "move_id": self.id,
389
+ "name": self.env._(
390
+ "Summary for tax amount %(percentage)s",
391
+ percentage=percentage,
392
+ ),
393
+ "price_unit": get_float(element, ".//ImponibileImporto"),
394
+ "tax_ids": tax.ids,
395
+ }
396
+ )
397
+ else:
398
+ messages_to_log.append(
399
+ Markup("<br/>").join(
400
+ (
401
+ self.env._(
402
+ "Tax not found for summary line "
403
+ "with percentage %(percentage)s.",
404
+ percentage=percentage,
405
+ ),
406
+ self._compose_info_message(element, "."),
407
+ )
408
+ )
409
+ )
410
+
411
+ return messages_to_log
412
+
413
+ def _l10n_it_edi_import_line(self, element, move_line, extra_info=None):
414
+ if extra_info is None:
415
+ extra_info = dict()
416
+ messages_to_log = []
417
+ company = move_line.company_id
418
+ import_detail_level = (
419
+ move_line.partner_id.l10n_it_edi_import_detail_level
420
+ or company.l10n_it_edi_import_detail_level
421
+ )
422
+ if import_detail_level == "min":
423
+ move_line.unlink()
424
+ line_description = " ".join(get_text(element, ".//Descrizione").split())
425
+ messages_to_log.append(
426
+ Markup("<br/>").join(
427
+ (
428
+ self.env._(
429
+ "Line with description %(line_description)s "
430
+ "has been skipped "
431
+ "because import detail level is minimum.",
432
+ line_description=line_description,
433
+ ),
434
+ self._compose_info_message(element, "."),
435
+ )
436
+ )
437
+ )
438
+ elif (
439
+ body_tree := extra_info.get("l10n_it_edi_ext_body_tree")
440
+ ) is not None and import_detail_level == "tax":
441
+ move_line.unlink()
442
+ tax_level_imported = extra_info.get("l10n_it_edi_ext_tax_level_imported")
443
+ if not tax_level_imported:
444
+ for summary_line in body_tree.xpath(".//DatiBeniServizi/DatiRiepilogo"):
445
+ messages_to_log += self._l10n_it_edi_ext_import_summary_line(
446
+ summary_line, extra_info=extra_info
447
+ )
448
+ extra_info["l10n_it_edi_ext_tax_level_imported"] = True
449
+ elif import_detail_level == "max":
450
+ # Admin. ref.
451
+ if admin_ref := get_text(element, ".//RiferimentoAmministrazione"):
452
+ move_line.l10n_it_edi_admin_ref = admin_ref
453
+
454
+ vals = {
455
+ "line_number": int(get_text(element, ".//NumeroLinea")),
456
+ "service_type": get_text(element, ".//TipoCessionePrestazione"),
457
+ "name": " ".join(get_text(element, ".//Descrizione").split()),
458
+ "qty": float(get_text(element, ".//Quantita") or 0),
459
+ "uom": get_text(element, ".//UnitaMisura"),
460
+ "period_start_date": get_date(element, ".//DataInizioPeriodo"),
461
+ "period_end_date": get_date(element, ".//DataFinePeriodo"),
462
+ "unit_price": get_float(element, ".//PrezzoUnitario"),
463
+ "total_price": get_float(element, ".//PrezzoTotale"),
464
+ "tax_amount": get_float(element, ".//AliquotaIVA"),
465
+ "wt_amount": get_text(element, ".//Ritenuta"),
466
+ "tax_kind": get_text(element, ".//Natura").upper(),
467
+ "invoice_line_id": move_line.id,
468
+ "invoice_id": move_line.move_id.id,
469
+ }
470
+ einvoice_line = self.env["l10n_it_edi.line"].create(vals)
471
+
472
+ if elements_code := element.xpath(".//CodiceArticolo"):
473
+ self.env["l10n_it_edi.article_code"].create(
474
+ [
475
+ {
476
+ "name": get_text(element_code, ".//CodiceTipo"),
477
+ "code_val": get_text(element_code, ".//CodiceValore"),
478
+ "l10n_it_edi_line_id": einvoice_line.id,
479
+ }
480
+ for element_code in elements_code
481
+ ]
482
+ )
483
+
484
+ if elements_discount := element.xpath(".//ScontoMaggiorazione"):
485
+ self.env["l10n_it_edi.discount_rise_price"].create(
486
+ [
487
+ {
488
+ "name": get_text(element_discount, ".//Tipo"),
489
+ "percentage": get_float(element_discount, ".//Percentuale"),
490
+ "amount": get_float(element_discount, ".//Importo"),
491
+ "l10n_it_edi_line_id": einvoice_line.id,
492
+ }
493
+ for element_discount in elements_discount
494
+ ]
495
+ )
496
+
497
+ if elements_other_data := element.xpath(".//AltriDatiGestionali"):
498
+ self.env["l10n_it_edi.line_other_data"].create(
499
+ [
500
+ {
501
+ "name": get_text(element_other_data, ".//TipoDato"),
502
+ "text_ref": get_text(
503
+ element_other_data, ".//RiferimentoTesto"
504
+ ),
505
+ "num_ref": get_float(
506
+ element_other_data, ".//RiferimentoNumero"
507
+ ),
508
+ "date_ref": get_date(
509
+ element_other_data, ".//RiferimentoData"
510
+ ),
511
+ "l10n_it_edi_line_id": einvoice_line.id,
512
+ }
513
+ for element_other_data in elements_other_data
514
+ ]
515
+ )
516
+
517
+ messages_to_log += super()._l10n_it_edi_import_line(
518
+ element, move_line, extra_info=extra_info
519
+ )
520
+ else:
521
+ raise UserError(
522
+ self.env._(
523
+ "Import detail level %(import_detail_level)s not supported.\n"
524
+ "Please set an import detail level in company %(company)s.",
525
+ import_detail_level=import_detail_level,
526
+ company=company.name,
527
+ )
528
+ )
529
+ return messages_to_log
530
+
531
+ def _l10n_it_edi_ext_check_amount(self, amount, edi_amount, message):
532
+ if (
533
+ edi_amount
534
+ and float_compare(
535
+ amount,
536
+ abs(edi_amount),
537
+ precision_rounding=self.currency_id.rounding,
538
+ )
539
+ != 0
540
+ ):
541
+ return message
542
+
543
+ def _l10n_it_edi_check_amount_untaxed(self):
544
+ return self._l10n_it_edi_ext_check_amount(
545
+ self.amount_untaxed - self.l10n_it_edi_rounding,
546
+ self.l10n_it_edi_amount_untaxed,
547
+ self.env._(
548
+ "Untaxed amount (%(amount_untaxed)s}) "
549
+ "minus rounding (%(rounding)s}) "
550
+ "does not match with "
551
+ "e-invoice untaxed amount %(edi_amount_untaxed)s)",
552
+ amount_untaxed=self.amount_untaxed,
553
+ rounding=self.l10n_it_edi_rounding,
554
+ edi_amount_untaxed=self.l10n_it_edi_amount_untaxed,
555
+ ),
556
+ )
557
+
558
+ def _l10n_it_edi_check_amount_tax(self):
559
+ return self._l10n_it_edi_ext_check_amount(
560
+ self.amount_tax,
561
+ self.l10n_it_edi_amount_tax,
562
+ self.env._(
563
+ "Taxed amount (%(tax_amount)s}) "
564
+ "does not match with "
565
+ "e-invoice taxed amount (%(edi_tax_amount)s)",
566
+ tax_amount=self.amount_tax,
567
+ edi_tax_amount=self.l10n_it_edi_amount_tax,
568
+ ),
569
+ )
570
+
571
+ def _l10n_it_edi_check_amount_total(self):
572
+ return self._l10n_it_edi_ext_check_amount(
573
+ self.amount_total,
574
+ self.l10n_it_edi_amount_total,
575
+ self.env._(
576
+ "Total amount (%(total_amount)s) "
577
+ "does not match with "
578
+ "e-invoice total amount (%(edi_total_amount)s)",
579
+ total_amount=self.amount_total,
580
+ edi_total_amount=self.l10n_it_edi_amount_total,
581
+ ),
582
+ )
583
+
584
+ def _l10n_it_edi_extend_partner_info(self, partner_role, partner_info):
585
+ if partner_role == "buyer":
586
+ partner_info_xpath = "//CessionarioCommittente"
587
+ elif partner_role == "seller":
588
+ partner_info_xpath = "//CedentePrestatore"
589
+ elif partner_role == "tax_representative":
590
+ partner_info_xpath = "//RappresentanteFiscale"
591
+ else:
592
+ raise UserError(
593
+ self.env._(
594
+ "Role %(role)s is not supported for partner creation/update",
595
+ role=partner_role,
596
+ )
597
+ )
598
+
599
+ partner_info.update(
600
+ {
601
+ "city_xpath": f"{partner_info_xpath}//Comune",
602
+ "codice_fiscale_xpath": f"{partner_info_xpath}//CodiceFiscale",
603
+ "country_code_xpath": f"{partner_info_xpath}//IdPaese",
604
+ "email_xpath": f"{partner_info_xpath}//Email",
605
+ "eori_code_xpath": f"{partner_info_xpath}//CodEORI",
606
+ "first_name_xpath": f"{partner_info_xpath}//Nome",
607
+ "last_name_xpath": f"{partner_info_xpath}//Cognome",
608
+ "name_xpath": f"{partner_info_xpath}//Denominazione",
609
+ "phone_xpath": f"{partner_info_xpath}//Telefono",
610
+ "register_code_xpath": f"{partner_info_xpath}//NumeroIscrizioneAlbo",
611
+ "register_regdate_xpath": f"{partner_info_xpath}//DataIscrizioneAlbo",
612
+ "register_state_xpath": f"{partner_info_xpath}//ProvinciaAlbo",
613
+ "register_xpath": f"{partner_info_xpath}//AlboProfessionale",
614
+ "state_xpath": f"{partner_info_xpath}//Provincia",
615
+ "street_number_xpath": f"{partner_info_xpath}//NumeroCivico",
616
+ "street_xpath": f"{partner_info_xpath}//Indirizzo",
617
+ "vat_xpath": f"{partner_info_xpath}//IdCodice",
618
+ "zip_xpath": f"{partner_info_xpath}//CAP",
619
+ }
620
+ )
621
+
622
+ @api.model
623
+ def _l10n_it_buyer_seller_info(self):
624
+ buyer_seller_info = super()._l10n_it_buyer_seller_info()
625
+ for role, partner_info in buyer_seller_info.items():
626
+ self._l10n_it_edi_extend_partner_info(role, partner_info)
627
+ return buyer_seller_info
628
+
629
+ def _l10n_it_edi_extension_get_partner_info_by_role(self, tree, role):
630
+ if role in ("buyer", "seller"):
631
+ buyer_seller_info = self._l10n_it_buyer_seller_info()
632
+ partner_info = buyer_seller_info[role]
633
+ else:
634
+ partner_info = dict()
635
+ self._l10n_it_edi_extend_partner_info(role, partner_info)
636
+ return partner_info
637
+
638
+ def _l10n_it_edi_extension_prepare_partner_values(self, tree, role):
639
+ if partner_info := self._l10n_it_edi_extension_get_partner_info_by_role(
640
+ tree, role
641
+ ):
642
+ vals = dict()
643
+ for field_name, partner_info_xpath in [
644
+ ("city", "city_xpath"),
645
+ ("email", "email_xpath"),
646
+ ("l10n_edi_it_eori_code", "eori_code_xpath"),
647
+ ("l10n_edi_it_register_code", "register_code_xpath"),
648
+ ("l10n_edi_it_register", "register_xpath"),
649
+ ("l10n_edi_it_register_regdate", "register_regdate_xpath"),
650
+ ("l10n_it_codice_fiscale", "codice_fiscale_xpath"),
651
+ ("phone", "phone_xpath"),
652
+ ("vat", "vat_xpath"),
653
+ ("zip", "zip_xpath"),
654
+ ]:
655
+ if value := get_text(tree, partner_info[partner_info_xpath]):
656
+ vals[field_name] = value
657
+
658
+ country_code = get_text(tree, partner_info["country_code_xpath"])
659
+ if country := self.env["res.country"].search(
660
+ [
661
+ ("code", "=", country_code),
662
+ ],
663
+ limit=1,
664
+ ):
665
+ vals["country_id"] = country.id
666
+
667
+ if province := get_text(tree, partner_info["state_xpath"]):
668
+ if found_province := self.env["res.country.state"].search(
669
+ [
670
+ ("code", "=", province),
671
+ ("country_id", "=", country.id),
672
+ ],
673
+ limit=1,
674
+ ):
675
+ vals["state_id"] = found_province.id
676
+ else:
677
+ message = self.env._(
678
+ "Province (%(province)s) not present in your system",
679
+ province=province,
680
+ )
681
+ self.sudo().message_post(body=message)
682
+
683
+ if register_province := get_text(
684
+ tree, partner_info["register_state_xpath"]
685
+ ):
686
+ if found_province := self.env["res.country.state"].search(
687
+ [
688
+ ("code", "=", register_province),
689
+ ("country_id", "=", country.id),
690
+ ],
691
+ limit=1,
692
+ ):
693
+ vals["l10n_edi_it_register_province_id"] = found_province.id
694
+ else:
695
+ message = self.env._(
696
+ "Register Province (%(register_province)s) not present in "
697
+ "your system",
698
+ register_province=register_province,
699
+ )
700
+ self.sudo().message_post(body=message)
701
+
702
+ if address_parts := list(
703
+ filter(
704
+ None,
705
+ [
706
+ get_text(tree, partner_info["street_xpath"]),
707
+ get_text(tree, partner_info["street_number_xpath"]),
708
+ ],
709
+ )
710
+ ):
711
+ vals["street"] = " ".join(address_parts)
712
+
713
+ if name := get_text(tree, partner_info["name_xpath"]):
714
+ vals["name"] = name
715
+ vals["is_company"] = True
716
+ if first_name := get_text(tree, partner_info["first_name_xpath"]):
717
+ vals["firstname"] = first_name
718
+ if last_name := get_text(tree, partner_info["last_name_xpath"]):
719
+ vals["lastname"] = last_name
720
+ else:
721
+ vals = dict()
722
+ return vals
723
+
724
+ def _l10n_it_edi_extension_create_partner(self, invoice_data, role):
725
+ partner_values = self._l10n_it_edi_extension_prepare_partner_values(
726
+ invoice_data,
727
+ role,
728
+ )
729
+ if partner_values:
730
+ partner = self.env["res.partner"].create(partner_values)
731
+ else:
732
+ partner = self.env["res.partner"].browse()
733
+ return partner
734
+
735
+ def _l10n_it_edi_import_invoice(self, invoice, data, is_new):
736
+ invoice = super()._l10n_it_edi_import_invoice(invoice, data, is_new)
737
+
738
+ body_tree = data["xml_tree"]
739
+ is_incoming = self.is_purchase_document(include_receipts=True)
740
+ partner_role = "seller" if is_incoming else "buyer"
741
+ if (
742
+ invoice
743
+ and invoice.partner_id
744
+ and not invoice.partner_id.l10n_edi_it_electronic_invoice_no_contact_update
745
+ ):
746
+ self._l10n_it_edi_update_partner(
747
+ body_tree, partner_role, invoice.partner_id
748
+ )
749
+ elif (
750
+ invoice
751
+ and not invoice.partner_id
752
+ and self.env.company.l10n_edi_it_create_partner
753
+ ):
754
+ invoice.partner_id = self._l10n_it_edi_extension_create_partner(
755
+ body_tree,
756
+ partner_role,
757
+ )
758
+
759
+ if tax_representative := self._l10n_it_edi_extension_create_partner(
760
+ body_tree,
761
+ "tax_representative",
762
+ ):
763
+ invoice.l10n_it_edi_tax_representative_id = tax_representative
764
+
765
+ return invoice