odoo-addon-l10n-es-verifactu-oca 15.0.1.0.0.2__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.

Potentially problematic release.


This version of odoo-addon-l10n-es-verifactu-oca might be problematic. Click here for more details.

Files changed (74) hide show
  1. odoo/addons/l10n_es_verifactu_oca/README.rst +154 -0
  2. odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
  3. odoo/addons/l10n_es_verifactu_oca/__manifest__.py +48 -0
  4. odoo/addons/l10n_es_verifactu_oca/data/account_fiscal_position_template_data.xml +129 -0
  5. odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
  6. odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +14 -0
  7. odoo/addons/l10n_es_verifactu_oca/data/mail_activity_data.xml +11 -0
  8. odoo/addons/l10n_es_verifactu_oca/data/verifactu_map_data.xml +120 -0
  9. odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +207 -0
  10. odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
  11. odoo/addons/l10n_es_verifactu_oca/hooks.py +43 -0
  12. odoo/addons/l10n_es_verifactu_oca/i18n/ca.po +1630 -0
  13. odoo/addons/l10n_es_verifactu_oca/i18n/ca_ES.po +1599 -0
  14. odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1640 -0
  15. odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1673 -0
  16. odoo/addons/l10n_es_verifactu_oca/models/__init__.py +16 -0
  17. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +40 -0
  18. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position_template.py +18 -0
  19. odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
  20. odoo/addons/l10n_es_verifactu_oca/models/account_move.py +556 -0
  21. odoo/addons/l10n_es_verifactu_oca/models/aeat_mixin.py +163 -0
  22. odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
  23. odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
  24. odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +33 -0
  25. odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +30 -0
  26. odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
  27. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +401 -0
  28. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +121 -0
  29. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +35 -0
  30. odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +66 -0
  31. odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +449 -0
  32. odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +24 -0
  33. odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.rst +18 -0
  34. odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.rst +19 -0
  35. odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.rst +1 -0
  36. odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.rst +4 -0
  37. odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.rst +15 -0
  38. odoo/addons/l10n_es_verifactu_oca/readme/USAGE.rst +1 -0
  39. odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +22 -0
  40. odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
  41. odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
  42. odoo/addons/l10n_es_verifactu_oca/static/description/index.html +508 -0
  43. odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +5 -0
  44. odoo/addons/l10n_es_verifactu_oca/tests/common.py +304 -0
  45. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_1.json +35 -0
  46. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_2.json +35 -0
  47. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
  48. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
  49. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
  50. odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +451 -0
  51. odoo/addons/l10n_es_verifactu_oca/tests/test_account_journal.py +78 -0
  52. odoo/addons/l10n_es_verifactu_oca/tests/test_account_move_reversal.py +93 -0
  53. odoo/addons/l10n_es_verifactu_oca/tests/test_res_partner.py +48 -0
  54. odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +350 -0
  55. odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +30 -0
  56. odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +28 -0
  57. odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +219 -0
  58. odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
  59. odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +55 -0
  60. odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
  61. odoo/addons/l10n_es_verifactu_oca/views/res_partner_view.xml +27 -0
  62. odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +47 -0
  63. odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +48 -0
  64. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +149 -0
  65. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +124 -0
  66. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +20 -0
  67. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +53 -0
  68. odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +42 -0
  69. odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +1 -0
  70. odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
  71. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/METADATA +171 -0
  72. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/RECORD +74 -0
  73. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/WHEEL +5 -0
  74. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,401 @@
1
+ # Copyright 2025 ForgeFlow S.L.
2
+ # Copyright 2025 Tecnativa - Pedro M. Baeza
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
4
+ import datetime
5
+ import json
6
+ import logging
7
+
8
+ from requests import Session
9
+ from zeep import Client, Settings
10
+ from zeep.plugins import HistoryPlugin
11
+ from zeep.transports import Transport
12
+
13
+ from odoo import _, api, fields, models
14
+ from odoo.exceptions import UserError
15
+ from odoo.tools import split_every
16
+
17
+ _logger = logging.getLogger(__name__)
18
+
19
+ VERIFACTU_SEND_STATES = [
20
+ ("not_sent", "Not sent"),
21
+ ("correct", "Sent and Correct"),
22
+ ("incorrect", "Sent and Incorrect"),
23
+ ("accepted_with_errors", "Sent and accepted with errors"),
24
+ ]
25
+
26
+ VERIFACTU_STATE_MAPPING = {
27
+ "Correcto": "correct",
28
+ "Incorrecto": "incorrect",
29
+ "AceptadoConErrores": "accepted_with_errors",
30
+ }
31
+
32
+
33
+ class VerifactuInvoiceEntry(models.Model):
34
+ _name = "verifactu.invoice.entry"
35
+ _description = "VERI*FACTU invoice entry"
36
+ _order = "id desc"
37
+ _rec_name = "document_hash"
38
+
39
+ verifactu_chaining_id = fields.Many2one(
40
+ "verifactu.chaining", string="Chaining", ondelete="restrict", required=True
41
+ )
42
+ model = fields.Char(readonly=True, required=True)
43
+ document_id = fields.Many2oneReference(
44
+ string="Document", model_field="model", readonly=True, index=True, required=True
45
+ )
46
+ document_name = fields.Char(readonly=True)
47
+ previous_invoice_entry_id = fields.Many2one(
48
+ "verifactu.invoice.entry", string="Previous Invoice Entry", readonly=True
49
+ )
50
+ company_id = fields.Many2one(
51
+ "res.company", string="Company", required=True, readonly=True
52
+ )
53
+ document_hash = fields.Char(required=True, readonly=True)
54
+ aeat_json_data = fields.Text(
55
+ string="AEAT JSON Data",
56
+ help="Generated JSON data to send to AEAT",
57
+ readonly=True,
58
+ )
59
+ send_state = fields.Selection(
60
+ selection=VERIFACTU_SEND_STATES,
61
+ compute="_compute_send_state",
62
+ default="not_sent",
63
+ readonly=True,
64
+ store=True,
65
+ copy=False,
66
+ help="Indicates the state of this document in relation with the "
67
+ "presentation to VERI*FACTU.",
68
+ )
69
+ send_attempt = fields.Integer(
70
+ default=0, help="Number of attempts to send this document."
71
+ )
72
+ response_line_ids = fields.One2many(
73
+ "verifactu.invoice.entry.response.line",
74
+ "entry_id",
75
+ string="Responses",
76
+ help="Responses from VERI*FACTU after sending the documents.",
77
+ )
78
+ last_error_code = fields.Char(compute="_compute_last_error_code", store=True)
79
+ previous_hash = fields.Char(
80
+ related="previous_invoice_entry_id.document_hash",
81
+ readonly=True,
82
+ string="Previous Hash",
83
+ )
84
+ entry_type = fields.Selection(
85
+ selection=[
86
+ ("register", "Register"),
87
+ ("modify", "Modify"),
88
+ ("cancel", "Cancel"),
89
+ ],
90
+ default="register",
91
+ required=True,
92
+ )
93
+ last_response_line_id = fields.Many2one(
94
+ "verifactu.invoice.entry.response.line",
95
+ string="Last Response Line",
96
+ readonly=True,
97
+ )
98
+
99
+ @api.depends("response_line_ids", "response_line_ids.send_state")
100
+ def _compute_send_state(self):
101
+ for rec in self:
102
+ rec.send_state = "not_sent"
103
+ last_response = rec.last_response_line_id
104
+ if last_response:
105
+ rec.send_state = last_response.send_state
106
+
107
+ @api.depends("response_line_ids", "response_line_ids.error_code")
108
+ def _compute_last_error_code(self):
109
+ """Compute the last error code from the response lines."""
110
+ for rec in self:
111
+ if rec.last_response_line_id:
112
+ rec.last_error_code = rec.last_response_line_id.error_code
113
+ else:
114
+ rec.last_error_code = ""
115
+
116
+ @property
117
+ def document(self):
118
+ return self.env[self.model].browse(self.document_id).exists()
119
+
120
+ @api.model
121
+ def _cron_send_documents_to_verifactu(self):
122
+ batch_limit = self.env["verifactu.mixin"]._get_verifactu_batch()
123
+ for chaining in self.env["verifactu.chaining"].search([]):
124
+ self.env.cr.execute(
125
+ """
126
+ SELECT id FROM verifactu_invoice_entry AS vsq
127
+ WHERE vsq.send_state in ('not_sent', 'incorrect')
128
+ AND vsq.verifactu_chaining_id = %s
129
+ ORDER BY id
130
+ FOR UPDATE NOWAIT
131
+ """,
132
+ [chaining.id],
133
+ )
134
+ entries_to_send_ids = [entry[0] for entry in self.env.cr.fetchall()]
135
+ for entries_batch_ids in split_every(batch_limit, entries_to_send_ids):
136
+ records_to_send = self.browse(entries_batch_ids)
137
+ send_date = fields.Datetime.now()
138
+ threshold_time = send_date - datetime.timedelta(seconds=240)
139
+ # Look for documents where we have to send as an incident
140
+ outdated_records = records_to_send.filtered(
141
+ lambda r: r.document.verifactu_registration_date < threshold_time
142
+ )
143
+ current_records = records_to_send - outdated_records
144
+ outdated_records.with_context(
145
+ verifactu_incident=True
146
+ )._send_documents_to_verifactu()
147
+ current_records._send_documents_to_verifactu()
148
+ return True
149
+
150
+ def _get_verifactu_aeat_header(self):
151
+ """Builds VERI*FACTU send header
152
+
153
+ :param tipo_comunicacion String 'A0': new reg, 'A1': modification
154
+ :param cancellation Bool True when the communitacion es for document
155
+ cancellation
156
+ :return Dict with header data depending on cancellation
157
+ """
158
+ self.ensure_one()
159
+ if not self.company_id.vat:
160
+ raise UserError(
161
+ _("No VAT configured for the company '{}'").format(self.company_id.name)
162
+ )
163
+ header = {
164
+ "ObligadoEmision": {
165
+ "NombreRazon": self.company_id.name[0:120],
166
+ "NIF": self.company_id.partner_id._parse_aeat_vat_info()[2],
167
+ },
168
+ }
169
+ incident = self.env.context.get("verifactu_incident", False)
170
+ if incident:
171
+ header.update({"RemisionVoluntaria": {"Incidencia": "S"}})
172
+ return header
173
+
174
+ def _bind_verifactu_service(self, client, port_name, address=None):
175
+ self.ensure_one()
176
+ service = client._get_service("sfVerifactu")
177
+ port = client._get_port(service, port_name)
178
+ address = address or port.binding_options["address"]
179
+ return client.create_service(port.binding.name, address)
180
+
181
+ def _connect_verifactu_params_aeat(self):
182
+ self.ensure_one()
183
+ agency = self.company_id.tax_agency_id
184
+ if not agency:
185
+ # We use spanish agency by default to keep old behavior with
186
+ # ir.config parameters. In the future it might be good to reinforce
187
+ # to explicitly set a tax agency in the company by raising an error
188
+ # here.
189
+ agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
190
+ return agency._connect_params_verifactu(self.company_id)
191
+
192
+ def _connect_verifactu(self):
193
+ self.ensure_one()
194
+ public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates(
195
+ company=self.company_id
196
+ )
197
+ if not public_crt or not private_key:
198
+ raise UserError(
199
+ _("Please, configure the VERI*FACTU certificates for your company")
200
+ )
201
+ params = self._connect_verifactu_params_aeat()
202
+ session = Session()
203
+ session.cert = (public_crt, private_key)
204
+ transport = Transport(session=session)
205
+ history = HistoryPlugin()
206
+ settings = Settings(forbid_entities=False)
207
+ client = Client(
208
+ wsdl=params["wsdl"],
209
+ transport=transport,
210
+ plugins=[history],
211
+ settings=settings,
212
+ )
213
+ return self._bind_verifactu_service(
214
+ client, params["port_name"], params["address"]
215
+ )
216
+
217
+ def _process_response_line_doc_vals(
218
+ self,
219
+ verifactu_response=False,
220
+ verifactu_response_line=False,
221
+ response_line=False,
222
+ previous_response_line=False,
223
+ header_sent=False,
224
+ ):
225
+ estado_registro = verifactu_response_line["EstadoRegistro"]
226
+ doc_vals = {
227
+ "aeat_header_sent": json.dumps(header_sent, indent=4),
228
+ }
229
+ doc_vals["verifactu_return"] = verifactu_response_line
230
+ send_error = False
231
+ if hasattr(verifactu_response_line, "CodigoErrorRegistro"):
232
+ send_error = "{} | {}".format(
233
+ str(verifactu_response_line["CodigoErrorRegistro"]),
234
+ str(verifactu_response_line["DescripcionErrorRegistro"]),
235
+ )
236
+ # si ya ha devuelto previamente registro duplicado, parseamos el estado
237
+ # del registro duplicado para dejar la factura correcta o incorrecta
238
+ if (
239
+ verifactu_response_line["CodigoErrorRegistro"] == 3000
240
+ and previous_response_line
241
+ and (
242
+ previous_response_line.error_code == "3000"
243
+ and previous_response_line.send_state == "incorrect"
244
+ )
245
+ ):
246
+ registroDuplicado = verifactu_response_line["RegistroDuplicado"]
247
+ estado_registro = registroDuplicado["EstadoRegistroDuplicado"]
248
+ # en duplicados devuelve Correcta en vez de Correcto...
249
+ if estado_registro == "Correcta":
250
+ estado_registro = "Correcto"
251
+ response_line.send_state = "correct"
252
+ elif registroDuplicado["CodigoErrorRegistro"]:
253
+ # en duplicados devuelve AceptadaConErrores en vez de AceptadoConErrores...
254
+ if estado_registro == "AceptadaConErrores":
255
+ estado_registro = "AceptadoConErrores"
256
+ response_line.send_state = "accepted_with_errors"
257
+ send_error = "{} | {}".format(
258
+ str(registroDuplicado["CodigoErrorRegistro"]),
259
+ str(registroDuplicado["DescripcionErrorRegistro"]),
260
+ )
261
+ if estado_registro == "Correcto":
262
+ doc_vals.update(
263
+ {
264
+ "aeat_state": "sent",
265
+ "verifactu_csv": verifactu_response["CSV"],
266
+ "aeat_send_failed": False,
267
+ }
268
+ )
269
+ elif estado_registro == "AceptadoConErrores":
270
+ doc_vals.update(
271
+ {
272
+ "aeat_state": "sent_w_errors",
273
+ "verifactu_csv": verifactu_response["CSV"],
274
+ "aeat_send_failed": True,
275
+ }
276
+ )
277
+ else:
278
+ doc_vals["aeat_send_failed"] = True
279
+ doc_vals["aeat_send_error"] = send_error
280
+ if response_line.document_id:
281
+ response_line.document.write(doc_vals)
282
+ return doc_vals
283
+
284
+ def _send_documents_to_verifactu(self):
285
+ if not self:
286
+ return False
287
+ rec = self[0]
288
+ header = rec._get_verifactu_aeat_header()
289
+ registro_factura_list = []
290
+ create_exception = False
291
+ for rec in self:
292
+ rec.send_attempt += 1
293
+ if rec.document:
294
+ inv_dict = rec.document._get_verifactu_invoice_dict()
295
+ registro_factura_list.append(inv_dict)
296
+ try:
297
+ serv = rec._connect_verifactu()
298
+ res = serv.RegFactuSistemaFacturacion(header, registro_factura_list)
299
+ except Exception as e:
300
+ _logger.error("Error sending documents to VERI*FACTU: %s", e)
301
+ res = {}
302
+ create_exception = True
303
+ response_name = ""
304
+ response = (
305
+ self.env["verifactu.invoice.entry.response"]
306
+ .sudo()
307
+ .create(
308
+ {
309
+ "header": json.dumps(header),
310
+ "name": response_name,
311
+ "invoice_data": json.dumps(registro_factura_list),
312
+ "response": res,
313
+ "verifactu_csv": "CSV" in res and res["CSV"] or _("-"),
314
+ }
315
+ )
316
+ )
317
+ response.complete_open_activity_on_exception()
318
+ if create_exception:
319
+ if not response.date_response:
320
+ response.date_response = fields.Datetime.now()
321
+ response.create_activity_on_exception()
322
+ create_response_activity = self._create_response_lines(
323
+ response=response, header=header, verifactu_response=res
324
+ )
325
+ updated_response_name = _("VERI*FACTU sending")
326
+ if create_exception:
327
+ updated_response_name = _("Connection error with VERI*FACTU")
328
+ elif create_response_activity:
329
+ updated_response_name = _("Incorrect invoices sent to VERI*FACTU")
330
+ response.name = updated_response_name
331
+ if create_response_activity:
332
+ response.create_send_response_activity()
333
+ return True
334
+
335
+ def _create_response_lines(
336
+ self, response=False, header=False, verifactu_response=False
337
+ ):
338
+ create_response_activity = False
339
+ # the returned object doesn't have `get` method, so use this form
340
+ verifactu_response_lines = (
341
+ "RespuestaLinea" in verifactu_response
342
+ and verifactu_response["RespuestaLinea"]
343
+ or []
344
+ )
345
+ models = self.env["verifactu.mixin"]._get_verifactu_reference_models()
346
+ for verifactu_response_line in verifactu_response_lines:
347
+ invoice_num = verifactu_response_line["IDFactura"]["NumSerieFactura"]
348
+ document = False
349
+ for model in models:
350
+ document = self.env[model].search(
351
+ [
352
+ ("name", "=", invoice_num),
353
+ ("id", "in", self.mapped("document_id")),
354
+ ],
355
+ limit=1,
356
+ )
357
+ if document:
358
+ break
359
+
360
+ # Skip if document not found
361
+ if not document:
362
+ continue
363
+
364
+ # Find the verifactu.invoice entry for this document
365
+ verifactu_invoice_entry = document.last_verifactu_invoice_entry_id
366
+
367
+ # Skip if no verifactu invoice entry found - this should not happen
368
+ # in normal flow but can happen in tests with mocked responses
369
+ if not verifactu_invoice_entry:
370
+ continue
371
+
372
+ previous_response_line = document.last_verifactu_response_line_id
373
+ send_state = VERIFACTU_STATE_MAPPING[
374
+ verifactu_response_line["EstadoRegistro"]
375
+ ]
376
+ vals = {
377
+ "entry_id": verifactu_invoice_entry.id,
378
+ "model": verifactu_invoice_entry.model,
379
+ "document_id": verifactu_invoice_entry.document_id,
380
+ "response": verifactu_response_line,
381
+ "entry_response_id": response.id,
382
+ "send_state": send_state,
383
+ "error_code": "CodigoErrorRegistro" in verifactu_response_line
384
+ and str(verifactu_response_line["CodigoErrorRegistro"])
385
+ or "",
386
+ }
387
+ response_line = (
388
+ self.env["verifactu.invoice.entry.response.line"].sudo().create(vals)
389
+ )
390
+ document.last_verifactu_response_line_id = response_line
391
+ verifactu_invoice_entry.last_response_line_id = response_line
392
+ self._process_response_line_doc_vals(
393
+ verifactu_response=verifactu_response,
394
+ verifactu_response_line=verifactu_response_line,
395
+ response_line=response_line,
396
+ previous_response_line=previous_response_line,
397
+ header_sent=header,
398
+ )
399
+ if send_state != "correct":
400
+ create_response_activity = True
401
+ return create_response_activity
@@ -0,0 +1,121 @@
1
+ # Copyright 2025 ForgeFlow S.L.
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import _, fields, models
4
+
5
+ VERIFACTU_STATE_MAPPING = {
6
+ "Correcto": "correct",
7
+ "Incorrecto": "incorrect",
8
+ "AceptadoConErrores": "accepted_with_errors",
9
+ }
10
+
11
+
12
+ class VerifactuInvoiceEntryResponse(models.Model):
13
+ _name = "verifactu.invoice.entry.response"
14
+ _description = "VERI*FACTU Send Response"
15
+ _inherit = ["mail.activity.mixin", "mail.thread"]
16
+ _order = "id desc"
17
+
18
+ header = fields.Text()
19
+ name = fields.Char()
20
+ invoice_data = fields.Text()
21
+ response = fields.Text()
22
+ verifactu_csv = fields.Text(string="VERI*FACTU CSV")
23
+ date_response = fields.Datetime(readonly=True)
24
+ activity_type_id = fields.Many2one(
25
+ "mail.activity.type",
26
+ string="Activity Type",
27
+ compute="_compute_activity_type_id",
28
+ store=True,
29
+ )
30
+ response_line_ids = fields.One2many(
31
+ "verifactu.invoice.entry.response.line",
32
+ "entry_response_id",
33
+ string="Response lines",
34
+ )
35
+
36
+ def _compute_activity_type_id(self):
37
+ for record in self:
38
+ activity = self.env["mail.activity"].search(
39
+ [
40
+ ("res_model", "=", "verifactu.invoice.entry.response"),
41
+ ("res_id", "=", record.id),
42
+ ],
43
+ limit=1,
44
+ )
45
+ record.activity_type_id = activity.activity_type_id if activity else False
46
+
47
+ def create_activity_on_exception(self):
48
+ model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
49
+ exception_activity_type = self.env.ref(
50
+ "l10n_es_verifactu_oca.mail_activity_data_exception"
51
+ )
52
+ activity_vals = []
53
+ responsible_group = self.env.ref(
54
+ "l10n_es_verifactu_oca.group_verifactu_responsible"
55
+ )
56
+ users = responsible_group.users
57
+ for record in self:
58
+ existing = self.env["mail.activity"].search_count(
59
+ [
60
+ ("activity_type_id", "=", exception_activity_type.id),
61
+ ("res_model", "=", "verifactu.invoice.entry.response"),
62
+ ]
63
+ )
64
+ if not existing:
65
+ user = users[:1] or self.env.user
66
+ activity_vals.append(
67
+ {
68
+ "res_model_id": model_id,
69
+ "res_model": "verifactu.invoice.entry.response",
70
+ "res_id": record.id,
71
+ "activity_type_id": exception_activity_type.id,
72
+ "user_id": user.id,
73
+ "summary": _("Check connection error with VERI*FACTU"),
74
+ "note": _(
75
+ "There has been an error when trying to connect to "
76
+ "VERI*FACTU"
77
+ ),
78
+ }
79
+ )
80
+ if activity_vals:
81
+ return self.env["mail.activity"].create(activity_vals)
82
+ return False
83
+
84
+ def create_send_response_activity(self):
85
+ activity_type = self.env.ref("mail.mail_activity_data_warning")
86
+ model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
87
+ activity_vals = []
88
+ responsible_group = self.env.ref(
89
+ "l10n_es_verifactu_oca.group_verifactu_responsible"
90
+ )
91
+ users = responsible_group.users
92
+ for record in self:
93
+ user = users[:1] or self.env.user
94
+ activity_vals.append(
95
+ {
96
+ "activity_type_id": activity_type.id,
97
+ "user_id": user.id,
98
+ "res_id": record.id,
99
+ "res_model": "verifactu.invoice.entry.response",
100
+ "res_model_id": model_id,
101
+ "summary": _("Check incorrect invoices from VERI*FACTU"),
102
+ "note": _("There is an error with one or more invoices"),
103
+ }
104
+ )
105
+ return self.env["mail.activity"].create(activity_vals)
106
+
107
+ def complete_open_activity_on_exception(self):
108
+ exception_activity_type = self.env.ref(
109
+ "l10n_es_verifactu_oca.mail_activity_data_exception"
110
+ )
111
+ for _record in self:
112
+ activity = self.env["mail.activity"].search(
113
+ [
114
+ ("activity_type_id", "=", exception_activity_type.id),
115
+ ("res_model", "=", "verifactu.invoice.entry.response"),
116
+ ],
117
+ )
118
+ for act in activity:
119
+ if act.state != "done":
120
+ act.action_done()
121
+ return True
@@ -0,0 +1,35 @@
1
+ # Copyright 2025 ForgeFlow S.L.
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+
4
+ from odoo import fields, models
5
+
6
+ from ..models.verifactu_invoice_entry import VERIFACTU_SEND_STATES
7
+
8
+
9
+ class VerifactuInvoiceEntryResponseLine(models.Model):
10
+ _name = "verifactu.invoice.entry.response.line"
11
+ _description = "VERI*FACTU send log"
12
+ _order = "id desc"
13
+
14
+ entry_id = fields.Many2one(comodel_name="verifactu.invoice.entry", required=True)
15
+ entry_response_id = fields.Many2one("verifactu.invoice.entry.response")
16
+ model = fields.Char(readonly=True)
17
+ document_id = fields.Many2oneReference(
18
+ string="Document", model_field="model", readonly=True, index=True
19
+ )
20
+ response = fields.Text()
21
+ send_state = fields.Selection(
22
+ selection=VERIFACTU_SEND_STATES,
23
+ string="VERI*FACTU send state",
24
+ default="not_sent",
25
+ readonly=True,
26
+ copy=False,
27
+ help="Indicates the state of this document in relation with the "
28
+ "presentation to VERI*FACTU.",
29
+ )
30
+ verifactu_csv = fields.Text(related="entry_response_id.verifactu_csv")
31
+ error_code = fields.Char()
32
+
33
+ @property
34
+ def document(self):
35
+ return self.env[self.model].browse(self.document_id).exists()
@@ -0,0 +1,66 @@
1
+ # Copyright 2024 Aures TIC - Almudena de La Puente <almudena@aurestic.es>
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+ from odoo import _, api, exceptions, fields, models
4
+
5
+
6
+ class AeatVerifactuMap(models.Model):
7
+ _name = "verifactu.map"
8
+ _description = "VERI*FACTU mapping"
9
+
10
+ name = fields.Char(string="Model", required=True)
11
+ date_from = fields.Date()
12
+ date_to = fields.Date()
13
+ map_lines = fields.One2many(
14
+ comodel_name="verifactu.map.line",
15
+ inverse_name="verifactu_map_id",
16
+ string="Lines",
17
+ )
18
+
19
+ @api.constrains("date_from", "date_to")
20
+ def _unique_date_range(self):
21
+ for record in self:
22
+ record._unique_date_range_one()
23
+
24
+ def _unique_date_range_one(self):
25
+ # Based in l10n_es_aeat module
26
+ domain = [("id", "!=", self.id)]
27
+ if self.date_from and self.date_to:
28
+ domain += [
29
+ "|",
30
+ "&",
31
+ ("date_from", "<=", self.date_to),
32
+ ("date_from", ">=", self.date_from),
33
+ "|",
34
+ "&",
35
+ ("date_to", "<=", self.date_to),
36
+ ("date_to", ">=", self.date_from),
37
+ "|",
38
+ "&",
39
+ ("date_from", "=", False),
40
+ ("date_to", ">=", self.date_from),
41
+ "|",
42
+ "&",
43
+ ("date_to", "=", False),
44
+ ("date_from", "<=", self.date_to),
45
+ ]
46
+ elif self.date_from:
47
+ domain += [("date_to", ">=", self.date_from)]
48
+ elif self.date_to:
49
+ domain += [("date_from", "<=", self.date_to)]
50
+ date_lst = self.search(domain)
51
+ if date_lst:
52
+ raise exceptions.UserError(
53
+ _("Error! The dates of the record overlap with an existing " "record.")
54
+ )
55
+
56
+
57
+ class AeatVerifactuMapLines(models.Model):
58
+ _name = "verifactu.map.line"
59
+ _description = "VERI*FACTU mapping line"
60
+
61
+ code = fields.Char(required=True)
62
+ name = fields.Char()
63
+ taxes = fields.Many2many(comodel_name="account.tax.template")
64
+ verifactu_map_id = fields.Many2one(
65
+ comodel_name="verifactu.map", string="Parent mapping", ondelete="cascade"
66
+ )