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.
- odoo/addons/l10n_it_edi_extension/README.rst +493 -0
- odoo/addons/l10n_it_edi_extension/__init__.py +951 -0
- odoo/addons/l10n_it_edi_extension/__manifest__.py +37 -0
- odoo/addons/l10n_it_edi_extension/controllers/__init__.py +1 -0
- odoo/addons/l10n_it_edi_extension/controllers/portal.py +27 -0
- odoo/addons/l10n_it_edi_extension/data/FoglioStileAssoSoftware.xsl +3150 -0
- odoo/addons/l10n_it_edi_extension/data/invoice_it_template.xml +50 -0
- odoo/addons/l10n_it_edi_extension/data/res.city.it.code.csv +13898 -0
- odoo/addons/l10n_it_edi_extension/i18n/l10n_it_edi_extension.pot +1167 -0
- odoo/addons/l10n_it_edi_extension/i18n/l10n_it_edi_fatturapa.pot +44 -0
- odoo/addons/l10n_it_edi_extension/models/__init__.py +15 -0
- odoo/addons/l10n_it_edi_extension/models/account_journal.py +152 -0
- odoo/addons/l10n_it_edi_extension/models/account_move.py +765 -0
- odoo/addons/l10n_it_edi_extension/models/account_move_line.py +10 -0
- odoo/addons/l10n_it_edi_extension/models/ir_attachment.py +37 -0
- odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_activity_progress.py +14 -0
- odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_article_code.py +15 -0
- odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_discount_rise_price.py +25 -0
- odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_line.py +48 -0
- odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_line_other_data.py +17 -0
- odoo/addons/l10n_it_edi_extension/models/l10n_it_edi_summary_data.py +77 -0
- odoo/addons/l10n_it_edi_extension/models/res_city_it_code.py +83 -0
- odoo/addons/l10n_it_edi_extension/models/res_company.py +62 -0
- odoo/addons/l10n_it_edi_extension/models/res_partner.py +64 -0
- odoo/addons/l10n_it_edi_extension/readme/CONFIGURE.md +45 -0
- odoo/addons/l10n_it_edi_extension/readme/CONTRIBUTORS.md +5 -0
- odoo/addons/l10n_it_edi_extension/readme/DESCRIPTION.md +160 -0
- odoo/addons/l10n_it_edi_extension/security/ir.model.access.csv +14 -0
- odoo/addons/l10n_it_edi_extension/static/description/icon.png +0 -0
- odoo/addons/l10n_it_edi_extension/static/description/index.html +832 -0
- odoo/addons/l10n_it_edi_extension/tests/__init__.py +2 -0
- odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT01234567890_FPR03.xml +166 -0
- odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT02780790107_11004.xml +216 -0
- odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT02780790107_11005.xml +224 -0
- odoo/addons/l10n_it_edi_extension/tests/import_xmls/IT05979361218_003.xml +107 -0
- odoo/addons/l10n_it_edi_extension/tests/import_xmls/test.png +0 -0
- odoo/addons/l10n_it_edi_extension/tests/import_xmls/xml_import.zip +0 -0
- odoo/addons/l10n_it_edi_extension/tests/test_fiscalcode.py +126 -0
- odoo/addons/l10n_it_edi_extension/tests/test_import_edi_extension_xml.py +350 -0
- odoo/addons/l10n_it_edi_extension/views/company_view.xml +41 -0
- odoo/addons/l10n_it_edi_extension/views/l10n_it_view.xml +164 -0
- odoo/addons/l10n_it_edi_extension/views/res_partner_view.xml +19 -0
- odoo/addons/l10n_it_edi_extension/wizards/__init__.py +2 -0
- odoo/addons/l10n_it_edi_extension/wizards/compute_fc.py +176 -0
- odoo/addons/l10n_it_edi_extension/wizards/compute_fc_view.xml +65 -0
- odoo/addons/l10n_it_edi_extension/wizards/l10n_it_edi_import_file_wizard.py +98 -0
- odoo/addons/l10n_it_edi_extension/wizards/l10n_it_edi_import_file_wizard.xml +46 -0
- odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/METADATA +513 -0
- odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/RECORD +51 -0
- odoo_addon_l10n_it_edi_extension-18.0.1.0.0.30.dist-info/WHEEL +5 -0
- 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>
|