Skip to content

Commit 3b28837

Browse files
committed
[REF] l10n_es_aeat_sii_match: Generalize its usage
Moving the contents to the SII mixin and doing the required adaptations: - Don't browse, but keep Odoo recordset in variables. - Rename variables. - Adapt DB layout to contain a many2one reference for the specified model. - Start to support other period types (trimesters). - Refactor code. - Include a new method for building the match result dictionary, to avoid code duplication and encapsulate the logic in one place. - Make it multi-record applicable. With this work, we are also promoting to both co-author and maintainers.
1 parent ef6271f commit 3b28837

11 files changed

Lines changed: 388 additions & 393 deletions

File tree

l10n_es_aeat_sii_match/README.rst

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
.. image:: https://odoo-community.org/readme-banner-image
2-
:target: https://odoo-community.org/get-involved?utm_source=readme
3-
:alt: Odoo Community Association
4-
51
===============================================================
62
Sistema de comprobación y contraste de facturas enviadas al SII
73
===============================================================
@@ -17,7 +13,7 @@ Sistema de comprobación y contraste de facturas enviadas al SII
1713
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
1814
:target: https://odoo-community.org/page/development-status
1915
:alt: Beta
20-
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
2117
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
2218
:alt: License: AGPL-3
2319
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--spain-lightgray.png?logo=github
@@ -69,6 +65,7 @@ Authors
6965
-------
7066

7167
* Studio73
68+
* Tecnativa
7269

7370
Contributors
7471
------------
@@ -85,6 +82,7 @@ Contributors
8582

8683
- Víctor Martínez
8784
- David Bañón Gil
85+
- Pedro M. Baeza
8886

8987
Maintainers
9088
-----------
@@ -105,10 +103,13 @@ promote its widespread use.
105103
.. |maintainer-Reyes4711-S73| image:: https://github.com/Reyes4711-S73.png?size=40px
106104
:target: https://github.com/Reyes4711-S73
107105
:alt: Reyes4711-S73
106+
.. |maintainer-pedrobaeza| image:: https://github.com/pedrobaeza.png?size=40px
107+
:target: https://github.com/pedrobaeza
108+
:alt: pedrobaeza
108109

109110
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
110111

111-
|maintainer-Abranes| |maintainer-Reyes4711-S73|
112+
|maintainer-Abranes| |maintainer-Reyes4711-S73| |maintainer-pedrobaeza|
112113

113114
This module is part of the `OCA/l10n-spain <https://github.com/OCA/l10n-spain/tree/18.0/l10n_es_aeat_sii_match>`_ project on GitHub.
114115

l10n_es_aeat_sii_match/__manifest__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"version": "18.0.1.0.1",
88
"category": "Accounting & Finance",
99
"website": "https://github.com/OCA/l10n-spain",
10-
"author": "Studio73, Odoo Community Association (OCA)",
10+
"author": "Studio73,Tecnativa,Odoo Community Association (OCA)",
1111
"license": "AGPL-3",
1212
"external_dependencies": {"python": ["deepdiff<8"]},
1313
"depends": ["l10n_es_aeat_sii_oca"],
@@ -17,5 +17,5 @@
1717
"views/aeat_sii_match_report.xml",
1818
],
1919
"installable": True,
20-
"maintainers": ["Abranes", "Reyes4711-S73"],
20+
"maintainers": ["Abranes", "Reyes4711-S73", "pedrobaeza"],
2121
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from . import account_move
22
from . import aeat_sii_match_difference
33
from . import aeat_sii_match_report
4+
from . import sii_mixin
Lines changed: 3 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,12 @@
1-
# Copyright 2018 Studio73 - Abraham Anes
2-
# Copyright 2019 Studio73 - Pablo Fuentes
3-
# Copyright 2022 Tecnativa - Pedro M. Baeza
1+
# Copyright 2026 Tecnativa - Pedro M. Baeza
42
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
53

6-
import json
7-
8-
from deepdiff import DeepDiff
9-
from zeep.helpers import serialize_object
10-
11-
from odoo import _, api, exceptions, fields, models
12-
from odoo.modules.registry import Registry
4+
from odoo import _, exceptions, models
135

146

157
class AccountMove(models.Model):
168
_inherit = "account.move"
179

18-
sii_match_sent = fields.Text(string="SII match sent", copy=False, readonly=True)
19-
sii_match_return = fields.Text(
20-
string="SII match return",
21-
copy=False,
22-
readonly=True,
23-
)
24-
sii_match_state = fields.Selection(
25-
string="Match state",
26-
readonly=True,
27-
copy=False,
28-
selection=[
29-
("1", "No testable"),
30-
("2", "In process of contrast"),
31-
("3", "Not contrasted"),
32-
("4", "Partially contrasted"),
33-
("5", "Contrasted"),
34-
],
35-
help="- No testable: The counterpart is not subscribed to SII "
36-
"the record will not be contrasted.\n"
37-
"- In process of contrast: AEAT is processing the data "
38-
"soon will be a result.\n"
39-
"- Not contrasted: The counterpart "
40-
"has not sent the invoice to SII, "
41-
"AEAT gives up to 4 months in order "
42-
"to contrast the information.\n"
43-
"- Partially contrasted: A invoice has been found "
44-
"but some data is different.\n"
45-
"- Contrasted: The counterpart has send "
46-
"the invoice to SII, all is OK.",
47-
)
48-
sii_contrast_state = fields.Selection(
49-
string="AEAT contrast state",
50-
readonly=True,
51-
copy=False,
52-
selection=[
53-
("correct", "Correct"),
54-
("no_exist", "Doesn't exist"),
55-
("partially", "Partially correct"),
56-
],
57-
)
58-
sii_match_difference_ids = fields.One2many(
59-
string="SII match differences",
60-
readonly=True,
61-
copy=False,
62-
comodel_name="l10n.es.aeat.sii.match.difference",
63-
inverse_name="invoice_id",
64-
)
65-
66-
def _get_diffs(self, odoo_values, sii_values):
67-
sii_values = json.loads(json.dumps(serialize_object(sii_values)))
68-
dp = self.env["decimal.precision"].precision_get("Account")
69-
res = []
70-
if not DeepDiff:
71-
raise exceptions.UserError(
72-
_(
73-
"You have not installed deepdiff library, "
74-
"please install it in order to use this feature"
75-
)
76-
)
77-
diff = DeepDiff(odoo_values, sii_values)
78-
differences = diff.get("type_changes", {})
79-
differences.update(diff.get("values_changed", {}))
80-
for label, value in list(differences.items()):
81-
sii_value = value["new_value"]
82-
odoo_value = value["old_value"]
83-
label = label.split("['")[-1].replace("']", "")
84-
if sii_value is not None:
85-
# We made an explicit case for TipoImpositivo because we get
86-
# always 2 numbers as strings, one with decimal point separator
87-
# and another without
88-
if label == "TipoImpositivo" or isinstance(odoo_value, float):
89-
sii_value = round(float(sii_value), dp)
90-
odoo_value = round(float(odoo_value), dp)
91-
elif isinstance(odoo_value, str):
92-
sii_value = sii_value.strip()
93-
odoo_value = odoo_value.strip()
94-
if sii_value != odoo_value:
95-
res.append(
96-
{
97-
"sii_field": label,
98-
"sii_return_field_value": sii_value,
99-
"sii_sent_field_value": odoo_value,
100-
}
101-
)
102-
return res
103-
104-
def _get_diffs_values(self, sii_values):
105-
self.ensure_one()
106-
res = []
107-
if self.aeat_content_sent:
108-
odoo_values = json.loads(self.aeat_content_sent)
109-
if self.move_type in ["out_invoice", "out_refund"]:
110-
res += self._get_diffs(
111-
odoo_values["FacturaExpedida"], sii_values["DatosFacturaEmitida"]
112-
)
113-
elif self.move_type in ["in_invoice", "in_refund"]:
114-
res += self._get_diffs(
115-
odoo_values["FacturaRecibida"], sii_values["DatosFacturaRecibida"]
116-
)
117-
return list((0, 0, r) for r in res)
118-
11910
def contrast_aeat(self):
12011
invalid_invoices = self.filtered(
12112
lambda invoice: not invoice.sii_csv
@@ -131,127 +22,4 @@ def contrast_aeat(self):
13122
invoice_names=", ".join(i.name for i in invalid_invoices),
13223
)
13324
)
134-
self._contrast_invoice_to_aeat()
135-
136-
def _get_contrast_invoice_dict_out(self):
137-
"""Build dict with data to send to AEAT WS for invoice types:
138-
out_invoice and out_refund.
139-
:return: invoices (dict) : Dict XML with data for this invoice.
140-
"""
141-
self.ensure_one()
142-
invoice_date = self._change_date_format(self.invoice_date)
143-
partner = self.partner_id.commercial_partner_id
144-
company = self.company_id
145-
ejercicio = self.date.year
146-
periodo = "%02d" % self.date.month
147-
number = self.name
148-
if self.thirdparty_invoice:
149-
number = self.thirdparty_number
150-
inv_dict = {
151-
"FiltroConsulta": {},
152-
"PeriodoLiquidacion": {"Ejercicio": ejercicio, "Periodo": periodo},
153-
"IDFactura": {
154-
"IDEmisorFactura": {"NIF": company.vat[2:]},
155-
"NumSerieFacturaEmisor": (number or "")[:60],
156-
"FechaExpedicionFacturaEmisor": invoice_date,
157-
},
158-
}
159-
if not partner.aeat_simplified_invoice:
160-
# Simplified invoices don't have counterpart
161-
inv_dict["Contraparte"] = {"NombreRazon": partner.name[0:120]}
162-
# Uso condicional de IDOtro/NIF
163-
inv_dict["Contraparte"].update(self._get_sii_identifier())
164-
return inv_dict
165-
166-
def _get_contrast_invoice_dict_in(self):
167-
"""Build dict with data to send to AEAT WS for invoice types:
168-
in_invoice and in_refund.
169-
:return: invoices (dict) : Dict XML with data for this invoice.
170-
"""
171-
self.ensure_one()
172-
invoice_date = self._change_date_format(self.invoice_date)
173-
ejercicio = self.date.year
174-
periodo = "%02d" % self.date.month
175-
inv_dict = {
176-
"FiltroConsulta": {},
177-
"IDFactura": {
178-
"IDEmisorFactura": {
179-
"NombreRazon": self.partner_id.commercial_partner_id.name[0:120],
180-
},
181-
"NumSerieFacturaEmisor": ((self.ref or "")[:60]),
182-
"FechaExpedicionFacturaEmisor": invoice_date,
183-
},
184-
"PeriodoLiquidacion": {"Ejercicio": ejercicio, "Periodo": periodo},
185-
}
186-
# Uso condicional de IDOtro/NIF
187-
ident = self._get_sii_identifier()
188-
inv_dict["IDFactura"]["IDEmisorFactura"].update(ident)
189-
return inv_dict
190-
191-
def _get_contrast_invoice_dict(self):
192-
self.ensure_one()
193-
self._aeat_check_exceptions()
194-
if self.move_type in ["out_invoice", "out_refund"]:
195-
return self._get_contrast_invoice_dict_out()
196-
elif self.move_type in ["in_invoice", "in_refund"]:
197-
return self._get_contrast_invoice_dict_in()
198-
return {}
199-
200-
def _contrast_invoice_to_aeat(self):
201-
for invoice in self.filtered(lambda i: i.state == "posted"):
202-
serv = invoice._connect_aeat(invoice.move_type)
203-
header = invoice._get_aeat_header(False, True)
204-
inv_vals = {}
205-
try:
206-
inv_dict = invoice._get_contrast_invoice_dict()
207-
inv_vals["sii_match_sent"] = json.dumps(inv_dict, indent=4)
208-
res_line = False
209-
if invoice.move_type in ["out_invoice", "out_refund"]:
210-
res = serv.ConsultaLRFacturasEmitidas(header, inv_dict)
211-
res_line = res["RegistroRespuestaConsultaLRFacturasEmitidas"][0]
212-
elif invoice.move_type in ["in_invoice", "in_refund"]:
213-
res = serv.ConsultaLRFacturasRecibidas(header, inv_dict)
214-
res_line = res["RegistroRespuestaConsultaLRFacturasRecibidas"][0]
215-
inv_vals.update(
216-
{"sii_contrast_state": "no_exist", "sii_match_state": False}
217-
)
218-
if res_line:
219-
if res_line["DatosPresentacion"]["CSV"] == self.sii_csv:
220-
cuadre_state = (
221-
res_line["EstadoFactura"]["EstadoCuadre"]
222-
if res_line["EstadoFactura"]
223-
else False
224-
)
225-
if cuadre_state:
226-
inv_vals.update(
227-
{
228-
"sii_match_state": res_line["EstadoFactura"][
229-
"EstadoCuadre"
230-
],
231-
"sii_contrast_state": "correct",
232-
}
233-
)
234-
diffs = invoice._get_diffs_values(res_line)
235-
if diffs:
236-
inv_vals["sii_match_difference_ids"] = diffs
237-
inv_vals.update({"sii_contrast_state": "partially"})
238-
invoice.sii_match_difference_ids.unlink()
239-
inv_vals["sii_match_return"] = json.dumps(
240-
serialize_object(res), indent=4
241-
)
242-
invoice.write(inv_vals)
243-
except Exception as fault:
244-
new_cr = Registry(self.env.cr.dbname).cursor()
245-
env = api.Environment(new_cr, self.env.uid, self.env.context)
246-
invoice = env["account.move"].browse(self.id)
247-
inv_vals.update(
248-
{
249-
"sii_match_return": repr(fault),
250-
"sii_contrast_state": False,
251-
"sii_match_state": False,
252-
}
253-
)
254-
invoice.write(inv_vals)
255-
new_cr.commit()
256-
new_cr.close()
257-
raise
25+
return super().contrast_aeat()

l10n_es_aeat_sii_match/models/aeat_sii_match_difference.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,15 @@ class SiiMatchDifferences(models.Model):
99
_name = "l10n.es.aeat.sii.match.difference"
1010
_description = "SII match difference"
1111

12-
invoice_id = fields.Many2one(string="Related invoice", comodel_name="account.move")
12+
invoice_id = fields.Many2oneReference(
13+
string="Document", model_field="model", readonly=True, index=True
14+
)
15+
model = fields.Char(default="account.move") # the default keeps the retro-compat.
1316
report_id = fields.Many2one(
14-
string="Related SII match report", comodel_name="l10n.es.aeat.sii.match.result"
17+
string="Related SII match report",
18+
comodel_name="l10n.es.aeat.sii.match.result",
19+
ondelete="cascade",
20+
index=True,
1521
)
1622
sii_field = fields.Char(string="SII field name", copy=False)
1723
sii_return_field_value = fields.Char(string="SII return field value", copy=False)

0 commit comments

Comments
 (0)