odoo-addon-l10n-es-verifactu-oca 16.0.1.0.0.22__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 (68) hide show
  1. odoo/addons/l10n_es_verifactu_oca/README.rst +151 -0
  2. odoo/addons/l10n_es_verifactu_oca/__init__.py +2 -0
  3. odoo/addons/l10n_es_verifactu_oca/__manifest__.py +43 -0
  4. odoo/addons/l10n_es_verifactu_oca/data/account_fiscal_position_template_data.xml +149 -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/neutralize.sql +2 -0
  9. odoo/addons/l10n_es_verifactu_oca/data/verifactu_map_data.xml +128 -0
  10. odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +207 -0
  11. odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
  12. odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1640 -0
  13. odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1598 -0
  14. odoo/addons/l10n_es_verifactu_oca/models/__init__.py +15 -0
  15. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +34 -0
  16. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position_template.py +18 -0
  17. odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
  18. odoo/addons/l10n_es_verifactu_oca/models/account_move.py +558 -0
  19. odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
  20. odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
  21. odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +26 -0
  22. odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +30 -0
  23. odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
  24. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +378 -0
  25. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +122 -0
  26. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +35 -0
  27. odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +66 -0
  28. odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +449 -0
  29. odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +24 -0
  30. odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.rst +18 -0
  31. odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.rst +16 -0
  32. odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.rst +1 -0
  33. odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.rst +4 -0
  34. odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.rst +15 -0
  35. odoo/addons/l10n_es_verifactu_oca/readme/USAGE.rst +1 -0
  36. odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +22 -0
  37. odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
  38. odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
  39. odoo/addons/l10n_es_verifactu_oca/static/description/index.html +505 -0
  40. odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +2 -0
  41. odoo/addons/l10n_es_verifactu_oca/tests/common.py +276 -0
  42. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_1.json +35 -0
  43. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_2.json +35 -0
  44. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
  45. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
  46. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
  47. odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +392 -0
  48. odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +348 -0
  49. odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +30 -0
  50. odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +28 -0
  51. odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +219 -0
  52. odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
  53. odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +26 -0
  54. odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
  55. odoo/addons/l10n_es_verifactu_oca/views/res_partner_view.xml +14 -0
  56. odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +47 -0
  57. odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +48 -0
  58. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +149 -0
  59. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +124 -0
  60. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +20 -0
  61. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +53 -0
  62. odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +42 -0
  63. odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +1 -0
  64. odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
  65. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/METADATA +168 -0
  66. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/RECORD +68 -0
  67. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/WHEEL +5 -0
  68. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/top_level.txt +1 -0
@@ -0,0 +1,392 @@
1
+ # Copyright 2024 Aures TIC - Almudena de La Puente
2
+ # Copyright 2024 FactorLibre - Luis J. Salvatierra
3
+ # Copyright 2025 ForgeFlow S.L.
4
+ # Copyright 2025 Process Control - Jorge Luis López
5
+ # Copyright 2025 Tecnativa - Pedro M. Baeza
6
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
7
+ import json
8
+ from datetime import datetime
9
+ from hashlib import sha256
10
+ from unittest.mock import MagicMock, patch
11
+ from urllib.parse import parse_qs, urlparse
12
+
13
+ from freezegun import freeze_time
14
+
15
+ from odoo import Command
16
+ from odoo.exceptions import UserError
17
+ from odoo.modules.module import get_resource_path
18
+
19
+ from .common import TestVerifactuCommon
20
+
21
+
22
+ class TestL10nEsAeatVerifactu(TestVerifactuCommon):
23
+ def test_verifactu_hash_code(self):
24
+ # based on AEAT VERI*FACTU documentation
25
+ # https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Veri-Factu_especificaciones_huella_hash_registros.pdf # noqa: B950
26
+ expected_hash = (
27
+ "6FA5B3FA912C71B23C274952AA00E13A5F40F0CEE466640FFAAD041FA8B79BFF"
28
+ )
29
+ verifactu_hash_string = (
30
+ "IDEmisorFactura=89890001K&"
31
+ "NumSerieFactura=12345678/G33&"
32
+ "FechaExpedicionFactura=01-01-2026&"
33
+ "TipoFactura=F1&"
34
+ "CuotaTotal=12.35&"
35
+ "ImporteTotal=123.45&"
36
+ "Huella=&"
37
+ "FechaHoraHusoGenRegistro=2026-01-01T19:20:30+01:00"
38
+ )
39
+ sha_hash_code = sha256(verifactu_hash_string.encode("utf-8"))
40
+ hash_code = sha_hash_code.hexdigest().upper()
41
+ self.assertEqual(hash_code, expected_hash)
42
+
43
+ def _create_and_test_invoice_verifactu_dict(
44
+ self, name, inv_type, lines, extra_vals, module=None
45
+ ):
46
+ vals = []
47
+ tax_names = []
48
+ for line in lines:
49
+ taxes = self.env["account.tax"]
50
+ for tax in line[1]:
51
+ if "." in tax:
52
+ xml_id = tax
53
+ else:
54
+ xml_id = "l10n_es.{}_account_tax_template_{}".format(
55
+ self.company.id, tax
56
+ )
57
+ taxes += self.env.ref(xml_id)
58
+ tax_names.append(tax)
59
+ vals.append({"price_unit": line[0], "taxes": taxes})
60
+ return self._compare_verifactu_dict(
61
+ "verifactu_{}_{}_dict.json".format(inv_type, "_".join(tax_names)),
62
+ name,
63
+ inv_type,
64
+ vals,
65
+ extra_vals=extra_vals,
66
+ module=module,
67
+ )
68
+
69
+ def _compare_verifactu_dict(
70
+ self, json_file, name, inv_type, lines, extra_vals=None, module=None
71
+ ):
72
+ """Helper method for creating an invoice according arguments, and
73
+ comparing the expected verifactu dict with .
74
+ """
75
+ module = module or "l10n_es_verifactu_oca"
76
+ vals = {
77
+ "name": name,
78
+ "partner_id": self.partner.id,
79
+ "invoice_date": "2026-01-01",
80
+ "move_type": inv_type,
81
+ "invoice_line_ids": [],
82
+ }
83
+ for line in lines:
84
+ vals["invoice_line_ids"].append(
85
+ Command.create(
86
+ {
87
+ "product_id": self.product.id,
88
+ "account_id": self.account_expense.id,
89
+ "name": "Test line",
90
+ "price_unit": line["price_unit"],
91
+ "quantity": 1,
92
+ "tax_ids": [(6, 0, line["taxes"].ids)],
93
+ },
94
+ )
95
+ )
96
+ if extra_vals:
97
+ vals.update(extra_vals)
98
+ invoice = self.env["account.move"].create(vals)
99
+ self._activate_certificate(self.certificate_password)
100
+ first_now = datetime(2026, 1, 1, 19, 20, 30)
101
+ with patch.object(self.env.cr, "now", lambda: first_now), freeze_time(
102
+ first_now
103
+ ):
104
+ invoice.action_post()
105
+ result_dict = invoice._get_verifactu_invoice_dict()
106
+ result_dict["RegistroAlta"].pop("FechaHoraHusoGenRegistro")
107
+ result_dict["RegistroAlta"].pop("TipoHuella")
108
+ result_dict["RegistroAlta"].pop("Huella")
109
+ path = get_resource_path(module, "tests/json", json_file)
110
+ if not path:
111
+ raise Exception("Incorrect JSON file: %s" % json_file)
112
+ with open(path, "r") as f:
113
+ expected_dict = json.loads(f.read())
114
+ self.assertEqual(expected_dict, result_dict)
115
+ entry = invoice.last_verifactu_invoice_entry_id
116
+ # Verify integration workflow
117
+ self.assertTrue(entry, "Invoice should have verifactu entry")
118
+ self.assertTrue(entry.aeat_json_data, "Should have JSON data")
119
+ return invoice
120
+
121
+ def test_get_verifactu_invoice_data(self):
122
+ mapping = [
123
+ (
124
+ "TEST001",
125
+ "out_invoice",
126
+ [(100, ["s_iva10b"]), (200, ["s_iva21s"])],
127
+ {
128
+ "fiscal_position_id": self.fp_nacional.id,
129
+ "verifactu_registration_key": self.fp_registration_key_01.id,
130
+ "verifactu_registration_date": "2026-01-01 19:20:30",
131
+ },
132
+ ),
133
+ (
134
+ "TEST002",
135
+ "out_refund",
136
+ [(100, ["s_iva10b"]), (100, ["s_iva10b"]), (200, ["s_iva21s"])],
137
+ {
138
+ "fiscal_position_id": self.fp_nacional.id,
139
+ "verifactu_registration_key": self.fp_registration_key_01.id,
140
+ "verifactu_registration_date": "2026-01-01 19:20:30",
141
+ },
142
+ ),
143
+ (
144
+ "TEST003",
145
+ "out_invoice",
146
+ [(200, ["s_iva21s", "s_req52"])],
147
+ {
148
+ "fiscal_position_id": self.fp_recargo.id,
149
+ "verifactu_registration_key": self.fp_registration_key_01.id,
150
+ "verifactu_registration_date": "2026-01-01 19:20:30",
151
+ },
152
+ ),
153
+ ]
154
+ for name, inv_type, lines, extra_vals in mapping:
155
+ self._create_and_test_invoice_verifactu_dict(
156
+ name, inv_type, lines, extra_vals
157
+ )
158
+ return
159
+
160
+ def test_verifactu_start_date(self):
161
+ self.company.verifactu_start_date = "2018-01-01"
162
+ invoice1 = self.invoice.copy({"invoice_date": "2019-01-01"})
163
+ self.assertTrue(invoice1.verifactu_enabled)
164
+ invoice2 = self.invoice.copy({"invoice_date": "2017-01-01"})
165
+ invoice2.invoice_date = "2017-01-01"
166
+ self.assertFalse(invoice2.verifactu_enabled)
167
+ self.company.verifactu_start_date = False
168
+ self.assertTrue(invoice2.verifactu_enabled)
169
+
170
+
171
+ class TestL10nEsAeatVerifactuQR(TestVerifactuCommon):
172
+ def _get_required_qr_params(self):
173
+ """Helper to generate the required QR code parameters."""
174
+ return {
175
+ "nif": self.invoice.company_id.partner_id._parse_aeat_vat_info()[2],
176
+ "numserie": self.invoice.name,
177
+ "fecha": self.invoice._get_verifactu_date(self.invoice.invoice_date),
178
+ "importe": f"{self.invoice.amount_total:.2f}", # noqa
179
+ }
180
+
181
+ def test_verifactu_qr_generation(self):
182
+ """
183
+ Test the generation of the QR code image for the invoice.
184
+ """
185
+ self._activate_certificate(self.certificate_password)
186
+ self.invoice.action_post()
187
+ qr_code = self.invoice.verifactu_qr
188
+ self.assertTrue(qr_code, "QR code should be generated for the invoice.")
189
+ self.assertIsInstance(qr_code, bytes, "QR code should be in bytes format.")
190
+
191
+ def test_verifactu_qr_url_format(self):
192
+ """
193
+ Test the format of the generated QR URL to ensure it meets expected criteria.
194
+ """
195
+ self._activate_certificate(self.certificate_password)
196
+ self.invoice.action_post()
197
+ qr_url = self.invoice.verifactu_qr_url
198
+ self.assertTrue(qr_url, "QR URL should be generated for the invoice.")
199
+ test_url = self.env.ref(
200
+ "l10n_es_aeat.aeat_tax_agency_spain"
201
+ ).verifactu_qr_base_url_test_address
202
+ self.assertTrue(test_url, "Test URL should not be empty.")
203
+ parsed_url = urlparse(qr_url)
204
+ actual_params = parse_qs(parsed_url.query)
205
+ expected_params = self._get_required_qr_params()
206
+ for key, expected_value in expected_params.items():
207
+ self.assertIn(
208
+ key, actual_params, f"QR URL should contain the parameter: {key}"
209
+ )
210
+ self.assertEqual(
211
+ actual_params[key][0],
212
+ str(expected_value),
213
+ f"QR URL parameter '{key}' should have value '{expected_value}', "
214
+ "got '{actual_params[key][0]}' instead.",
215
+ )
216
+
217
+ def test_verifactu_qr_code_generation_on_draft(self):
218
+ """
219
+ Ensure that the QR code is not generated for invoices in draft state.
220
+ """
221
+ qr_code = self.invoice.verifactu_qr
222
+ self.assertFalse(qr_code, "QR code should not be generated for draft invoices.")
223
+
224
+ def test_verifactu_qr_code_after_update(self):
225
+ """
226
+ Test that the QR code is regenerated if the invoice details are updated.
227
+ """
228
+ self._activate_certificate(self.certificate_password)
229
+ self.invoice.action_post()
230
+ original_qr_code = self.invoice.verifactu_qr
231
+ with self.assertRaises(UserError):
232
+ self.invoice.button_cancel()
233
+ self.invoice.button_draft()
234
+ self.invoice.write(
235
+ {
236
+ "invoice_line_ids": [
237
+ Command.create(
238
+ {
239
+ "product_id": self.product.id,
240
+ "account_id": self.account_expense.id,
241
+ "name": "Updated line",
242
+ "price_unit": 200,
243
+ "quantity": 1,
244
+ },
245
+ )
246
+ ]
247
+ }
248
+ )
249
+ self.invoice.action_post()
250
+ self.invoice.invalidate_model(["verifactu_qr_url", "verifactu_qr"])
251
+ updated_qr_code = self.invoice.verifactu_qr
252
+ self.assertNotEqual(
253
+ original_qr_code,
254
+ updated_qr_code,
255
+ "QR code should be regenerated after invoice update.",
256
+ )
257
+
258
+ def test_send_invoices_to_verifactu(self):
259
+ self._activate_certificate(self.certificate_password)
260
+ self.invoice.action_post()
261
+ with patch(
262
+ "odoo.addons.l10n_es_verifactu_oca.models."
263
+ "verifactu_invoice_entry.VerifactuInvoiceEntry._connect_verifactu"
264
+ ) as mock_connect:
265
+ mock_service = MagicMock()
266
+ module = "l10n_es_verifactu_oca"
267
+ json_file = "verifactu_mocked_response_1.json"
268
+ path = get_resource_path(module, "tests/json", json_file)
269
+ if not path:
270
+ raise Exception("Incorrect JSON file: %s" % json_file)
271
+ with open(path, "r") as f:
272
+ response_dict = json.loads(f.read())
273
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
274
+ mock_connect.return_value = mock_service
275
+ # Execute the cron job to send the invoice to VERI*FACTU
276
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
277
+ self.assertEqual(
278
+ self.invoice.aeat_state,
279
+ "sent",
280
+ "Invoice should be marked as sent after VERI*FACTU processing.",
281
+ )
282
+ self.assertEqual(
283
+ self.invoice.verifactu_csv,
284
+ "A-Y23JP3582934",
285
+ "CSV should be generated correctly after sending to VERI*FACTU.",
286
+ )
287
+
288
+
289
+ class TestVerifactuSendResponse(TestVerifactuCommon):
290
+ def test_create_activity_on_exception(self):
291
+ """
292
+ Creates an activity whenever the connection with VERI*FACTU
293
+ is not possible.
294
+ """
295
+ MailActivity = self.env["mail.activity"]
296
+ ActivityType = self.env.ref(
297
+ "l10n_es_verifactu_oca.mail_activity_data_exception"
298
+ )
299
+ # Send an invoice without a certificate
300
+ self.invoice.action_post()
301
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
302
+ self.assertEqual(self.invoice.aeat_state, "not_sent")
303
+ activity_1 = MailActivity.search(
304
+ [
305
+ ("activity_type_id", "=", ActivityType.id),
306
+ ("res_model", "=", "verifactu.invoice.entry.response"),
307
+ ]
308
+ )
309
+ self.assertTrue(activity_1, "An exception activity should have been created")
310
+ self.invoice.resend_verifactu()
311
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
312
+ activity_2 = MailActivity.search(
313
+ [
314
+ ("activity_type_id", "=", ActivityType.id),
315
+ ("res_model", "=", "verifactu.invoice.entry.response"),
316
+ ]
317
+ )
318
+ self.assertEqual(
319
+ len(activity_1),
320
+ len(activity_2),
321
+ "There should be only one exception activity created",
322
+ )
323
+ # Activate certificate and re-run the cron
324
+ self._activate_certificate(self.certificate_password)
325
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
326
+ activity_done = (
327
+ self.env["mail.activity"]
328
+ .with_context(active_test=False)
329
+ .search(
330
+ [
331
+ ("activity_type_id", "=", ActivityType.id),
332
+ ("res_model", "=", "verifactu.invoice.entry.response"),
333
+ ]
334
+ )
335
+ )
336
+ # todo: fix this, it's not activity_done.has_recommended_activites,
337
+ # should check if it's not visible anymore to the user
338
+ self.assertFalse(
339
+ activity_done.has_recommended_activities,
340
+ "The exception activity should not appear.",
341
+ )
342
+
343
+ def mock_verifactu_response(self, error_code, description):
344
+ """Recreates a verifactu response"""
345
+ return {
346
+ "CSV": "dummy-csv",
347
+ "RespuestaLinea": [
348
+ {
349
+ "IDFactura": {
350
+ "NumSerieFactura": self.invoice.name,
351
+ },
352
+ "EstadoRegistro": "AceptadoConErrores",
353
+ "CodigoErrorRegistro": error_code,
354
+ "DescripcionErrorRegistro": description,
355
+ }
356
+ ],
357
+ }
358
+
359
+ @patch(
360
+ "odoo.addons.l10n_es_verifactu_oca.models.verifactu_invoice_entry."
361
+ "VerifactuInvoiceEntry._connect_verifactu"
362
+ )
363
+ def test_create_send_activity(self, mock_connect):
364
+ """
365
+ Create an activity whenever the response from VERI*FACTU indicates
366
+ that incorrect invoices have been sent
367
+ """
368
+ MailActivity = self.env["mail.activity"]
369
+ ActivityType = self.env.ref("mail.mail_activity_data_warning")
370
+ mock_service = MagicMock()
371
+ module = "l10n_es_verifactu_oca"
372
+ json_file = "verifactu_mocked_response_2.json"
373
+ path = get_resource_path(module, "tests/json", json_file)
374
+ if not path:
375
+ raise Exception("Incorrect JSON file: %s" % json_file)
376
+ with open(path, "r") as f:
377
+ response_dict = json.loads(f.read())
378
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
379
+ mock_connect.return_value = mock_service
380
+ self.invoice.action_post()
381
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
382
+ activity = MailActivity.search(
383
+ [
384
+ ("activity_type_id", "=", ActivityType.id),
385
+ ("res_model", "=", "verifactu.invoice.entry.response"),
386
+ ("summary", "=", "Check incorrect invoices from VERI*FACTU"),
387
+ ]
388
+ )
389
+ self.assertTrue(
390
+ activity,
391
+ "A warning activity should be created for 'AceptadoConErrores' response",
392
+ )