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
157class 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 ()
0 commit comments