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,176 @@
1
+ # Copyright 2025 Simone Rubino
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+
4
+ import logging
5
+
6
+ from codicefiscale import build
7
+
8
+ from odoo import api, fields, models
9
+ from odoo.exceptions import UserError
10
+
11
+ _logger = logging.getLogger(__name__)
12
+
13
+
14
+ class WizardComputeFc(models.TransientModel):
15
+ _name = "wizard.compute.fc"
16
+ _description = "Compute Fiscal Code"
17
+ _rec_name = "fiscalcode_surname"
18
+
19
+ fiscalcode_surname = fields.Char("Surname", required=True, size=64)
20
+ fiscalcode_firstname = fields.Char("First name", required=True, size=64)
21
+ birth_date = fields.Date("Date of birth", required=True)
22
+ birth_city = fields.Many2one(
23
+ "res.city.it.code.distinct", required=True, string="City of birth"
24
+ )
25
+ birth_province = fields.Many2one(
26
+ "res.country.state", required=True, string="Province"
27
+ )
28
+ sex = fields.Selection([("M", "Male"), ("F", "Female")], required=True)
29
+
30
+ @api.onchange("birth_city")
31
+ def onchange_birth_city(self):
32
+ self.ensure_one()
33
+
34
+ it = self.env.ref("base.it").id
35
+ res = {
36
+ "value": {"birth_province": False},
37
+ }
38
+
39
+ if self.birth_city:
40
+ # SMELLS: Add a foreign key in "res_city_it_code"
41
+ # instead using the weak link "code" <-> "province".
42
+ #
43
+ city_ids = self.env["res.city.it.code"].search(
44
+ [("name", "=", self.birth_city.name)]
45
+ )
46
+ provinces = city_ids.mapped("province")
47
+ province_ids = self.env["res.country.state"].search(
48
+ [("country_id", "=", it), ("code", "in", provinces)]
49
+ )
50
+
51
+ if len(province_ids) == 1:
52
+ res["value"]["birth_province"] = province_ids.id
53
+
54
+ return res
55
+
56
+ def _get_national_code(self, birth_city, birth_prov, birth_date):
57
+ """
58
+ notes fields contains variation data while var_date may contain the
59
+ eventual date of the variation. notes may be:
60
+ - ORA: city changed name, name_var contains new name, national_code_var
61
+ contains the repeated national code.
62
+ There are some cities that contains two identical values, for
63
+ example PORTO (CO), has two ORA entries for G906 and G907, this
64
+ is rather unpredictable and the first value will be taken
65
+ - AGG: city has been aggregated to another one and doesn't exist
66
+ anymore. name_var and national_code_var contain recent data.
67
+ Some cities have particular cases, for example ALME' (BG) that
68
+ is listed as aggregate to another city since 1927 but gained
69
+ independence (creation_date) in 1948
70
+ - AGP: partially aggregated, city has been split and assigned to more
71
+ than one other cities. name_var and national_code_var contain
72
+ recent data. It's not possible to determine the correct code
73
+ for new city so the original code is returned
74
+ - AGT: temporarily aggregated to another city, if possible this gets
75
+ ignored. name_var and national_code_var contain recent data
76
+ - VED: reference to another city. This is assigned to cities that
77
+ changed name and were then subject to other changes.
78
+ """
79
+ cities = self.env["res.city.it.code"].search(
80
+ [("name", "=", birth_city), ("province", "=", birth_prov)],
81
+ order="creation_date ASC, var_date ASC, notes ASC",
82
+ )
83
+ if not cities or len(cities) == 0:
84
+ return ""
85
+ # Checks for any VED element
86
+ newcts = None
87
+ for ct in cities:
88
+ if ct.notes == "VED":
89
+ newcts = self.env["res.city.it.code"].search(
90
+ [("name", "=", ct.name_var)]
91
+ )
92
+ break
93
+ if newcts:
94
+ cities = newcts
95
+ return self._check_national_codes(birth_date, cities)
96
+
97
+ def _check_national_codes(self, birth_date, cities):
98
+ nc = ""
99
+ dtcostvar = None
100
+ for ct in cities:
101
+ if ct.creation_date and (
102
+ not dtcostvar or not ct.creation_date or dtcostvar < ct.creation_date
103
+ ):
104
+ dtcostvar = ct.creation_date
105
+ if not ct.notes:
106
+ nc = ct.national_code
107
+ elif ct.notes == "ORA" and (
108
+ not dtcostvar or not ct.var_date or dtcostvar < ct.var_date
109
+ ):
110
+ if not ct.var_date or ct.var_date <= birth_date:
111
+ nc = ct.national_code_var
112
+ elif not nc:
113
+ nc = ct.national_code
114
+ if ct.var_date:
115
+ dtcostvar = ct.var_date
116
+ elif ct.notes == "AGG" and (
117
+ not dtcostvar or not ct.var_date or dtcostvar < ct.var_date
118
+ ):
119
+ if not ct.var_date or ct.var_date <= birth_date:
120
+ nc = ct.national_code_var
121
+ elif not nc:
122
+ nc = ct.national_code
123
+ if ct.var_date:
124
+ dtcostvar = ct.var_date
125
+ elif ct.notes == "AGP" and (
126
+ not dtcostvar or not ct.var_date or dtcostvar < ct.var_date
127
+ ):
128
+ nc = ct.national_code
129
+ if ct.var_date:
130
+ dtcostvar = ct.var_date
131
+ elif ct.notes == "AGP" and (
132
+ not dtcostvar or not ct.var_date or dtcostvar < ct.var_date
133
+ ):
134
+ nc = ct.national_code
135
+
136
+ return nc
137
+
138
+ def compute_fc(self):
139
+ active_id = self._context.get("active_id")
140
+ partner = self.env["res.partner"].browse(active_id)
141
+ for f in self:
142
+ if (
143
+ not f.fiscalcode_surname
144
+ or not f.fiscalcode_firstname
145
+ or not f.birth_date
146
+ or not f.birth_city
147
+ or not f.sex
148
+ ):
149
+ raise UserError(self.env._("One or more fields are missing"))
150
+ nat_code = self._get_national_code(
151
+ f.birth_city.name, f.birth_province.code, f.birth_date
152
+ )
153
+ if not nat_code:
154
+ raise UserError(self.env._("National code is missing"))
155
+ c_f = build(
156
+ f.fiscalcode_surname,
157
+ f.fiscalcode_firstname,
158
+ f.birth_date,
159
+ f.sex,
160
+ nat_code,
161
+ )
162
+ if partner.l10n_it_codice_fiscale and partner.l10n_it_codice_fiscale != c_f:
163
+ raise UserError(
164
+ self.env._(
165
+ "Existing fiscal code %(partner_fiscalcode)s is different "
166
+ "from the computed one (%(compute)s). If you want to use"
167
+ " the computed one, remove the existing one"
168
+ )
169
+ % {
170
+ "partner_fiscalcode": partner.l10n_it_codice_fiscale,
171
+ "compute": c_f,
172
+ }
173
+ )
174
+ partner.l10n_it_codice_fiscale = c_f
175
+ partner.company_type = "person"
176
+ return {"type": "ir.actions.act_window_close"}
@@ -0,0 +1,65 @@
1
+ <?xml version="1.0" ?>
2
+ <odoo>
3
+ <record id="wizard_compute_fc_form" model="ir.ui.view">
4
+ <field name="name">wizard.compute.fc.form</field>
5
+ <field name="model">wizard.compute.fc</field>
6
+ <field name="type">form</field>
7
+ <field name="arch" type="xml">
8
+ <form string="Fiscal Code">
9
+ <h2>Individual Data</h2>
10
+ <group>
11
+ <group>
12
+ <field name="fiscalcode_surname" default_focus="1" />
13
+ <field name="fiscalcode_firstname" />
14
+ <field name="sex" widget="radio" />
15
+ </group>
16
+ <group>
17
+ <field name="birth_date" />
18
+ <field
19
+ name="birth_city"
20
+ options="{'no_create': True, 'no_open': True}"
21
+ />
22
+ <field
23
+ name="birth_province"
24
+ options="{'no_create': True, 'no_open': True}"
25
+ />
26
+ </group>
27
+ </group>
28
+ <footer>
29
+ <button
30
+ class="btn-primary"
31
+ type="object"
32
+ name="compute_fc"
33
+ string="Compute"
34
+ />
35
+ <button class="btn-default" special="cancel" string="Cancel" />
36
+ </footer>
37
+ </form>
38
+ </field>
39
+ </record>
40
+
41
+ <record id="action_compute_fc" model="ir.actions.act_window">
42
+ <field name="name">Compute Fiscal Code</field>
43
+ <field name="type">ir.actions.act_window</field>
44
+ <field name="res_model">wizard.compute.fc</field>
45
+ <field name="view_mode">form</field>
46
+ <field name="target">new</field>
47
+ </record>
48
+
49
+ <record id="res_partner_form_l10n_it_button" model="ir.ui.view">
50
+ <field name="name">res.partner.form.l10n.it.button</field>
51
+ <field name="model">res.partner</field>
52
+ <field name="inherit_id" ref="l10n_it_edi.res_partner_form_l10n_it" />
53
+ <field name="arch" type="xml">
54
+ <field name="l10n_it_codice_fiscale" position="after">
55
+ <button
56
+ name="%(l10n_it_edi_extension.action_compute_fc)d"
57
+ invisible="'IT' not in fiscal_country_codes or parent_id"
58
+ string="Compute FC"
59
+ type="action"
60
+ icon="fa-gear"
61
+ />
62
+ </field>
63
+ </field>
64
+ </record>
65
+ </odoo>
@@ -0,0 +1,98 @@
1
+ # Copyright 2025 Giuseppe Borruso - Dinamiche Aziendali srl
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+
4
+ import base64
5
+ import io
6
+ import logging
7
+ import os
8
+ import zipfile
9
+
10
+ from odoo import fields, models
11
+ from odoo.exceptions import UserError
12
+
13
+ _logger = logging.getLogger(__name__)
14
+
15
+
16
+ class EInvoiceImportFileWizard(models.TransientModel):
17
+ _name = "l10n_it_edi.import_file_wizard"
18
+ _description = "E-invoice Import Files Wizard"
19
+
20
+ l10n_it_edi_attachment = fields.Binary()
21
+ l10n_it_edi_attachment_filename = fields.Char()
22
+
23
+ def action_import(self):
24
+ self.ensure_one()
25
+ company = self.env.company
26
+ zip_binary = base64.b64decode(self.l10n_it_edi_attachment)
27
+ zip_io = io.BytesIO(zip_binary)
28
+ moves = self.env["account.move"]
29
+
30
+ with zipfile.ZipFile(zip_io, "r") as zip_ref:
31
+ for member in zip_ref.infolist():
32
+ if not member.is_dir():
33
+ with zip_ref.open(member) as file:
34
+ filename = os.path.basename(member.filename)
35
+ attachment_model = (
36
+ self.env["ir.attachment"].sudo().with_company(company)
37
+ )
38
+ existing_attachment = attachment_model.search_count(
39
+ [
40
+ ("name", "=", filename),
41
+ ("res_model", "=", "account.move"),
42
+ ("res_field", "=", "l10n_it_edi_attachment_file"),
43
+ ("company_id", "=", company.id),
44
+ ],
45
+ limit=1,
46
+ )
47
+
48
+ if existing_attachment:
49
+ message = f"E-invoice already exists: {filename}"
50
+ _logger.warning(message)
51
+ raise UserError(self.env._(message))
52
+
53
+ content = file.read()
54
+ attachment = attachment_model.create(
55
+ {
56
+ "name": filename,
57
+ "raw": content,
58
+ "type": "binary",
59
+ }
60
+ )
61
+
62
+ if not attachment._is_l10n_it_edi_import_file():
63
+ _logger.info(f"Skipping {filename}, not an XML/P7M file")
64
+ attachment.unlink()
65
+ continue
66
+
67
+ for file_data in attachment._decode_edi_l10n_it_edi(
68
+ filename, content
69
+ ):
70
+ move = (
71
+ self.env["account.move"]
72
+ .with_company(company)
73
+ .create({})
74
+ )
75
+ attachment.write(
76
+ {
77
+ "res_model": "account.move",
78
+ "res_id": move.id,
79
+ "res_field": "l10n_it_edi_attachment_file",
80
+ }
81
+ )
82
+
83
+ move.with_context(
84
+ account_predictive_bills_disable_prediction=True,
85
+ no_new_invoice=True,
86
+ ).message_post(attachment_ids=attachment.ids)
87
+
88
+ move._l10n_it_edi_import_invoice(move, file_data, True)
89
+ moves |= move
90
+
91
+ return {
92
+ "view_type": "form",
93
+ "name": "E-invoices",
94
+ "view_mode": "list,form",
95
+ "res_model": "account.move",
96
+ "type": "ir.actions.act_window",
97
+ "domain": [("id", "in", moves.ids)],
98
+ }
@@ -0,0 +1,46 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <odoo>
3
+ <record id="l10n_it_edi_import_file_wizard_form" model="ir.ui.view">
4
+ <field name="name">l10n_it_edi_import_file_wizard_form_view</field>
5
+ <field name="model">l10n_it_edi.import_file_wizard</field>
6
+ <field name="arch" type="xml">
7
+ <form>
8
+ <sheet>
9
+ <group>
10
+ <field
11
+ name="l10n_it_edi_attachment"
12
+ filename="l10n_it_edi_attachment_filename"
13
+ />
14
+ <field name="l10n_it_edi_attachment_filename" invisible="1" />
15
+ </group>
16
+ </sheet>
17
+ <footer>
18
+ <button
19
+ name="action_import"
20
+ type="object"
21
+ string="Import Files"
22
+ class="oe_highlight"
23
+ />
24
+ <button string="Cancel" class="btn-secondary" special="cancel" />
25
+ </footer>
26
+ </form>
27
+ </field>
28
+ </record>
29
+
30
+ <record id="action_l10n_it_edi_import_file_wizard" model="ir.actions.act_window">
31
+ <field name="name">E-invoice Import Files</field>
32
+ <field name="res_model">l10n_it_edi.import_file_wizard</field>
33
+ <field name="view_mode">form</field>
34
+ <field name="target">new</field>
35
+ <field
36
+ name="view_id"
37
+ ref="l10n_it_edi_extension.l10n_it_edi_import_file_wizard_form"
38
+ />
39
+ </record>
40
+
41
+ <menuitem
42
+ id="menu_l10n_it_edi_import_file_wizard"
43
+ action="l10n_it_edi_extension.action_l10n_it_edi_import_file_wizard"
44
+ parent="account.menu_finance_payables"
45
+ />
46
+ </odoo>