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,451 @@
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.exceptions import UserError
16
+ from odoo.modules.module import get_resource_path
17
+
18
+ from .common import TestVerifactuCommon
19
+
20
+
21
+ class TestL10nEsAeatVerifactu(TestVerifactuCommon):
22
+ def test_verifactu_hash_code(self):
23
+ # based on AEAT VERI*FACTU documentation
24
+ # https://www.agenciatributaria.es/static_files/AEAT_Desarrolladores/EEDD/IVA/VERI-FACTU/Veri-Factu_especificaciones_huella_hash_registros.pdf # noqa: B950
25
+ expected_hash = (
26
+ "6FA5B3FA912C71B23C274952AA00E13A5F40F0CEE466640FFAAD041FA8B79BFF"
27
+ )
28
+ verifactu_hash_string = (
29
+ "IDEmisorFactura=89890001K&"
30
+ "NumSerieFactura=12345678/G33&"
31
+ "FechaExpedicionFactura=01-01-2026&"
32
+ "TipoFactura=F1&"
33
+ "CuotaTotal=12.35&"
34
+ "ImporteTotal=123.45&"
35
+ "Huella=&"
36
+ "FechaHoraHusoGenRegistro=2026-01-01T19:20:30+01:00"
37
+ )
38
+ sha_hash_code = sha256(verifactu_hash_string.encode("utf-8"))
39
+ hash_code = sha_hash_code.hexdigest().upper()
40
+ self.assertEqual(hash_code, expected_hash)
41
+
42
+ def _create_and_test_invoice_verifactu_dict(
43
+ self, name, inv_type, lines, extra_vals, module=None
44
+ ):
45
+ vals = []
46
+ tax_names = []
47
+ for line in lines:
48
+ taxes = self.env["account.tax"]
49
+ for tax in line[1]:
50
+ if "." in tax:
51
+ xml_id = tax
52
+ else:
53
+ xml_id = "l10n_es.{}_account_tax_template_{}".format(
54
+ self.company.id, tax
55
+ )
56
+ taxes += self.env.ref(xml_id)
57
+ tax_names.append(tax)
58
+ vals.append({"price_unit": line[0], "taxes": taxes})
59
+ return self._compare_verifactu_dict(
60
+ "verifactu_{}_{}_dict.json".format(inv_type, "_".join(tax_names)),
61
+ name,
62
+ inv_type,
63
+ vals,
64
+ extra_vals=extra_vals,
65
+ module=module,
66
+ )
67
+
68
+ def _compare_verifactu_dict(
69
+ self, json_file, name, inv_type, lines, extra_vals=None, module=None
70
+ ):
71
+ """Helper method for creating an invoice according arguments, and
72
+ comparing the expected verifactu dict with .
73
+ """
74
+ module = module or "l10n_es_verifactu_oca"
75
+ vals = {
76
+ "name": name,
77
+ "partner_id": self.partner.id,
78
+ "invoice_date": "2026-01-01",
79
+ "move_type": inv_type,
80
+ "invoice_line_ids": [],
81
+ }
82
+ for line in lines:
83
+ vals["invoice_line_ids"].append(
84
+ (
85
+ 0,
86
+ 0,
87
+ {
88
+ "product_id": self.product.id,
89
+ "account_id": self.account_expense.id,
90
+ "name": "Test line",
91
+ "price_unit": line["price_unit"],
92
+ "quantity": 1,
93
+ "tax_ids": [(6, 0, line["taxes"].ids)],
94
+ },
95
+ )
96
+ )
97
+ if extra_vals:
98
+ vals.update(extra_vals)
99
+ invoice = self.env["account.move"].create(vals)
100
+ self._activate_certificate(self.certificate_password)
101
+ first_now = datetime(2026, 1, 1, 19, 20, 30)
102
+ with patch(
103
+ "odoo.addons.l10n_es_verifactu_oca.models.account_move.datetime"
104
+ ) as mock_datetime:
105
+ mock_datetime.now.return_value = first_now
106
+ with freeze_time(first_now):
107
+ invoice.action_post()
108
+ result_dict = invoice._get_verifactu_invoice_dict()
109
+ result_dict["RegistroAlta"].pop("FechaHoraHusoGenRegistro")
110
+ result_dict["RegistroAlta"].pop("TipoHuella")
111
+ result_dict["RegistroAlta"].pop("Huella")
112
+ path = get_resource_path(module, "tests/json", json_file)
113
+ if not path:
114
+ raise Exception("Incorrect JSON file: %s" % json_file)
115
+ with open(path, "r") as f:
116
+ expected_dict = json.loads(f.read())
117
+ self.assertEqual(expected_dict, result_dict)
118
+ entry = invoice.last_verifactu_invoice_entry_id
119
+ # Verify integration workflow
120
+ self.assertTrue(entry, "Invoice should have verifactu entry")
121
+ self.assertTrue(entry.aeat_json_data, "Should have JSON data")
122
+ return invoice
123
+
124
+ def test_get_verifactu_invoice_data(self):
125
+ mapping = [
126
+ (
127
+ "TEST001",
128
+ "out_invoice",
129
+ [(100, ["s_iva10b"]), (200, ["s_iva21s"])],
130
+ {
131
+ "fiscal_position_id": self.fp_nacional.id,
132
+ "verifactu_registration_key": self.fp_registration_key_01.id,
133
+ "verifactu_registration_date": "2026-01-01 19:20:30",
134
+ },
135
+ ),
136
+ (
137
+ "TEST002",
138
+ "out_refund",
139
+ [(100, ["s_iva10b"]), (100, ["s_iva10b"]), (200, ["s_iva21s"])],
140
+ {
141
+ "fiscal_position_id": self.fp_nacional.id,
142
+ "verifactu_registration_key": self.fp_registration_key_01.id,
143
+ "verifactu_registration_date": "2026-01-01 19:20:30",
144
+ },
145
+ ),
146
+ (
147
+ "TEST003",
148
+ "out_invoice",
149
+ [(200, ["s_iva21s", "s_req52"])],
150
+ {
151
+ "fiscal_position_id": self.fp_recargo.id,
152
+ "verifactu_registration_key": self.fp_registration_key_01.id,
153
+ "verifactu_registration_date": "2026-01-01 19:20:30",
154
+ },
155
+ ),
156
+ ]
157
+ for name, inv_type, lines, extra_vals in mapping:
158
+ self._create_and_test_invoice_verifactu_dict(
159
+ name, inv_type, lines, extra_vals
160
+ )
161
+ return
162
+
163
+ def test_verifactu_start_date(self):
164
+ self.company.verifactu_start_date = "2018-01-01"
165
+ invoice1 = self.invoice.copy({"invoice_date": "2019-01-01"})
166
+ self.assertTrue(invoice1.verifactu_enabled)
167
+ invoice2 = self.invoice.copy({"invoice_date": "2017-01-01"})
168
+ invoice2.invoice_date = "2017-01-01"
169
+ self.assertFalse(invoice2.verifactu_enabled)
170
+ self.company.verifactu_start_date = False
171
+ self.assertTrue(invoice2.verifactu_enabled)
172
+
173
+
174
+ class TestL10nEsAeatVerifactuQR(TestVerifactuCommon):
175
+ def _get_required_qr_params(self):
176
+ """Helper to generate the required QR code parameters."""
177
+ return {
178
+ "nif": self.invoice.company_id.partner_id._parse_aeat_vat_info()[2],
179
+ "numserie": self.invoice.name,
180
+ "fecha": self.invoice._get_verifactu_date(self.invoice.invoice_date),
181
+ "importe": f"{self.invoice.amount_total:.2f}", # noqa
182
+ }
183
+
184
+ def test_verifactu_qr_generation(self):
185
+ """
186
+ Test the generation of the QR code image for the invoice.
187
+ """
188
+ self._activate_certificate(self.certificate_password)
189
+ self.invoice.action_post()
190
+ qr_code = self.invoice.verifactu_qr
191
+ self.assertTrue(qr_code, "QR code should be generated for the invoice.")
192
+ self.assertIsInstance(qr_code, bytes, "QR code should be in bytes format.")
193
+
194
+ def test_verifactu_qr_url_format(self):
195
+ """
196
+ Test the format of the generated QR URL to ensure it meets expected criteria.
197
+ """
198
+ self._activate_certificate(self.certificate_password)
199
+ self.invoice.action_post()
200
+ qr_url = self.invoice.verifactu_qr_url
201
+ self.assertTrue(qr_url, "QR URL should be generated for the invoice.")
202
+ test_url = self.env.ref(
203
+ "l10n_es_aeat.aeat_tax_agency_spain"
204
+ ).verifactu_qr_base_url_test_address
205
+ self.assertTrue(test_url, "Test URL should not be empty.")
206
+ parsed_url = urlparse(qr_url)
207
+ actual_params = parse_qs(parsed_url.query)
208
+ expected_params = self._get_required_qr_params()
209
+ for key, expected_value in expected_params.items():
210
+ self.assertIn(
211
+ key, actual_params, f"QR URL should contain the parameter: {key}"
212
+ )
213
+ self.assertEqual(
214
+ actual_params[key][0],
215
+ str(expected_value),
216
+ f"QR URL parameter '{key}' should have value '{expected_value}', "
217
+ "got '{actual_params[key][0]}' instead.",
218
+ )
219
+
220
+ def test_verifactu_qr_code_generation_on_draft(self):
221
+ """
222
+ Ensure that the QR code is not generated for invoices in draft state.
223
+ """
224
+ qr_code = self.invoice.verifactu_qr
225
+ self.assertFalse(qr_code, "QR code should not be generated for draft invoices.")
226
+
227
+ def test_verifactu_qr_code_after_update(self):
228
+ """
229
+ Test that the QR code is regenerated if the invoice details are updated.
230
+ """
231
+ self._activate_certificate(self.certificate_password)
232
+ self.invoice.action_post()
233
+ original_qr_code = self.invoice.verifactu_qr
234
+ with self.assertRaises(UserError):
235
+ self.invoice.button_cancel()
236
+ self.invoice.button_draft()
237
+ self.invoice.write(
238
+ {
239
+ "invoice_line_ids": [
240
+ (
241
+ 0,
242
+ 0,
243
+ {
244
+ "product_id": self.product.id,
245
+ "account_id": self.account_expense.id,
246
+ "name": "Updated line",
247
+ "price_unit": 200,
248
+ "quantity": 1,
249
+ },
250
+ )
251
+ ]
252
+ }
253
+ )
254
+ self.invoice.action_post()
255
+ self.invoice.invalidate_model(["verifactu_qr_url", "verifactu_qr"])
256
+ updated_qr_code = self.invoice.verifactu_qr
257
+ self.assertNotEqual(
258
+ original_qr_code,
259
+ updated_qr_code,
260
+ "QR code should be regenerated after invoice update.",
261
+ )
262
+
263
+ def test_send_invoices_to_verifactu(self):
264
+ self._activate_certificate(self.certificate_password)
265
+ self.invoice.action_post()
266
+ with patch(
267
+ "odoo.addons.l10n_es_verifactu_oca.models."
268
+ "verifactu_invoice_entry.VerifactuInvoiceEntry._connect_verifactu"
269
+ ) as mock_connect:
270
+ mock_service = MagicMock()
271
+ module = "l10n_es_verifactu_oca"
272
+ json_file = "verifactu_mocked_response_1.json"
273
+ path = get_resource_path(module, "tests/json", json_file)
274
+ if not path:
275
+ raise Exception("Incorrect JSON file: %s" % json_file)
276
+ with open(path, "r") as f:
277
+ response_dict = json.loads(f.read())
278
+ # Update the response to match the actual invoice name
279
+ response_dict["RespuestaLinea"][0]["IDFactura"][
280
+ "NumSerieFactura"
281
+ ] = self.invoice.name
282
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
283
+ mock_connect.return_value = mock_service
284
+ # Execute the cron job to send the invoice to VERI*FACTU
285
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
286
+ self.assertEqual(
287
+ self.invoice.aeat_state,
288
+ "sent",
289
+ "Invoice should be marked as sent after VERI*FACTU processing.",
290
+ )
291
+ self.assertEqual(
292
+ self.invoice.verifactu_csv,
293
+ "A-Y23JP3582934",
294
+ "CSV should be generated correctly after sending to VERI*FACTU.",
295
+ )
296
+
297
+
298
+ class TestVerifactuSendResponse(TestVerifactuCommon):
299
+ def test_create_activity_on_exception(self):
300
+ """
301
+ Creates an activity whenever the connection with VERI*FACTU
302
+ is not possible.
303
+ """
304
+ MailActivity = self.env["mail.activity"]
305
+ ActivityType = self.env.ref(
306
+ "l10n_es_verifactu_oca.mail_activity_data_exception"
307
+ )
308
+ # Send an invoice without a certificate
309
+ self.invoice.action_post()
310
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
311
+ self.assertEqual(self.invoice.aeat_state, "not_sent")
312
+ activity_1 = MailActivity.search(
313
+ [
314
+ ("activity_type_id", "=", ActivityType.id),
315
+ ("res_model", "=", "verifactu.invoice.entry.response"),
316
+ ]
317
+ )
318
+ self.assertTrue(activity_1, "An exception activity should have been created")
319
+ self.invoice.resend_verifactu()
320
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
321
+ activity_2 = MailActivity.search(
322
+ [
323
+ ("activity_type_id", "=", ActivityType.id),
324
+ ("res_model", "=", "verifactu.invoice.entry.response"),
325
+ ]
326
+ )
327
+ self.assertEqual(
328
+ len(activity_1),
329
+ len(activity_2),
330
+ "There should be only one exception activity created",
331
+ )
332
+ # Activate certificate and re-run the cron
333
+ self._activate_certificate(self.certificate_password)
334
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
335
+ activity_done = (
336
+ self.env["mail.activity"]
337
+ .with_context(active_test=False)
338
+ .search(
339
+ [
340
+ ("activity_type_id", "=", ActivityType.id),
341
+ ("res_model", "=", "verifactu.invoice.entry.response"),
342
+ ]
343
+ )
344
+ )
345
+ # todo: fix this, it's not activity_done.has_recommended_activites,
346
+ # should check if it's not visible anymore to the user
347
+ self.assertFalse(
348
+ activity_done.has_recommended_activities,
349
+ "The exception activity should not appear.",
350
+ )
351
+
352
+ def mock_verifactu_response(self, error_code, description):
353
+ """Recreates a verifactu response"""
354
+ return {
355
+ "CSV": "dummy-csv",
356
+ "RespuestaLinea": [
357
+ {
358
+ "IDFactura": {
359
+ "NumSerieFactura": self.invoice.name,
360
+ },
361
+ "EstadoRegistro": "AceptadoConErrores",
362
+ "CodigoErrorRegistro": error_code,
363
+ "DescripcionErrorRegistro": description,
364
+ }
365
+ ],
366
+ }
367
+
368
+ @patch(
369
+ "odoo.addons.l10n_es_verifactu_oca.models.verifactu_invoice_entry."
370
+ "VerifactuInvoiceEntry._connect_verifactu"
371
+ )
372
+ def test_create_send_activity(self, mock_connect):
373
+ """
374
+ Create an activity whenever the response from VERI*FACTU indicates
375
+ that incorrect invoices have been sent
376
+ """
377
+ MailActivity = self.env["mail.activity"]
378
+ ActivityType = self.env.ref("mail.mail_activity_data_warning")
379
+ mock_service = MagicMock()
380
+ module = "l10n_es_verifactu_oca"
381
+ json_file = "verifactu_mocked_response_2.json"
382
+ path = get_resource_path(module, "tests/json", json_file)
383
+ if not path:
384
+ raise Exception("Incorrect JSON file: %s" % json_file)
385
+ with open(path, "r") as f:
386
+ response_dict = json.loads(f.read())
387
+ # Update the response to match the actual invoice name
388
+ response_dict["RespuestaLinea"][0]["IDFactura"][
389
+ "NumSerieFactura"
390
+ ] = self.invoice.name
391
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
392
+ mock_connect.return_value = mock_service
393
+ self.invoice.action_post()
394
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
395
+ activity = MailActivity.search(
396
+ [
397
+ ("activity_type_id", "=", ActivityType.id),
398
+ ("res_model", "=", "verifactu.invoice.entry.response"),
399
+ ("summary", "=", "Check incorrect invoices from VERI*FACTU"),
400
+ ]
401
+ )
402
+ self.assertTrue(
403
+ activity,
404
+ "A warning activity should be created for 'AceptadoConErrores' response",
405
+ )
406
+
407
+ def test_aeat_mixin_helper_methods(self):
408
+ """Test helper methods in aeat_mixin that need coverage"""
409
+ # Test _change_date_format
410
+ formatted_date = self.invoice._change_date_format("2024-03-15")
411
+ self.assertEqual(formatted_date, "15-03-2024")
412
+
413
+ def test_verifactu_configuration_checks(self):
414
+ """Test VERI*FACTU configuration validation methods"""
415
+ # Test without tax agency
416
+ self.company.tax_agency_id = False
417
+ with self.assertRaises(UserError) as cm:
418
+ self.invoice._check_verifactu_configuration()
419
+ self.assertIn("tax agency configured", str(cm.exception))
420
+
421
+ # Restore tax agency for next test
422
+ self.company.tax_agency_id = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
423
+
424
+ # Test without developer
425
+ self.company.verifactu_developer_id = False
426
+ with self.assertRaises(UserError) as cm:
427
+ self.invoice._check_verifactu_configuration()
428
+ self.assertIn("VERI*FACTU developer configured", str(cm.exception))
429
+
430
+ # Restore developer for next test
431
+ self.company.verifactu_developer_id = self.verifactu_developer
432
+
433
+ # Test with non-Spanish country
434
+ self.company.country_id = self.env.ref("base.us")
435
+ with self.assertRaises(UserError) as cm:
436
+ self.invoice._check_verifactu_configuration()
437
+ self.assertIn("not registered in Spain", str(cm.exception))
438
+
439
+ def test_verifactu_mixin_methods(self):
440
+ """Test verifactu_mixin methods for better coverage"""
441
+ # Test _get_verifactu_version
442
+ version = self.invoice._get_verifactu_version()
443
+ self.assertEqual(version, 1.0)
444
+
445
+ # Test _compute_verifactu_refund_type
446
+ self.invoice._compute_verifactu_refund_type()
447
+ self.assertFalse(self.invoice.verifactu_refund_type)
448
+
449
+ # Test _get_verifactu_accepted_tax_agencies
450
+ agencies = self.invoice._get_verifactu_accepted_tax_agencies()
451
+ self.assertIn("l10n_es_aeat.aeat_tax_agency_spain", agencies)
@@ -0,0 +1,78 @@
1
+ # Copyright 2025 Process Control - Jorge Luis López
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3
+
4
+ from odoo.exceptions import ValidationError
5
+
6
+ from .common import TestVerifactuCommon
7
+
8
+
9
+ class TestAccountJournal(TestVerifactuCommon):
10
+ def test_journal_hash_modification_validation(self):
11
+ """Test validation when disabling hash restriction on VERI*FACTU enabled journals"""
12
+ # Create a sale journal with VERI*FACTU enabled
13
+ journal = self.env["account.journal"].create(
14
+ {
15
+ "name": "Test Sale Journal",
16
+ "type": "sale",
17
+ "code": "TSJ",
18
+ "company_id": self.company.id,
19
+ "verifactu_enabled": True,
20
+ "restrict_mode_hash_table": True,
21
+ }
22
+ )
23
+
24
+ # Test that we cannot disable hash restriction on VERI*FACTU enabled sale journal
25
+ with self.assertRaises(ValidationError) as cm:
26
+ journal.write({"restrict_mode_hash_table": False})
27
+ self.assertIn("restricted hash modification", str(cm.exception))
28
+
29
+ # Test that creation fails with invalid combination
30
+ with self.assertRaises(ValidationError) as cm:
31
+ self.env["account.journal"].create(
32
+ {
33
+ "name": "Invalid Journal",
34
+ "type": "sale",
35
+ "code": "INV",
36
+ "company_id": self.company.id,
37
+ "verifactu_enabled": True,
38
+ "restrict_mode_hash_table": False,
39
+ }
40
+ )
41
+ self.assertIn("restricted hash modification", str(cm.exception))
42
+
43
+ def test_journal_non_sale_type_allowed(self):
44
+ """Test that non-sale journals can have hash restriction disabled"""
45
+ # Purchase journal should work without restrictions
46
+ journal = self.env["account.journal"].create(
47
+ {
48
+ "name": "Test Purchase Journal",
49
+ "type": "purchase",
50
+ "code": "TPJ",
51
+ "company_id": self.company.id,
52
+ "verifactu_enabled": True,
53
+ "restrict_mode_hash_table": False,
54
+ }
55
+ )
56
+ self.assertFalse(journal.restrict_mode_hash_table)
57
+
58
+ # Should be able to modify it too
59
+ journal.write({"restrict_mode_hash_table": True})
60
+ self.assertTrue(journal.restrict_mode_hash_table)
61
+
62
+ def test_journal_company_verifactu_disabled(self):
63
+ """Test that journals work when company VERI*FACTU is disabled"""
64
+ # Disable VERI*FACTU on company
65
+ self.company.verifactu_enabled = False
66
+
67
+ # Should be able to create sale journal without hash restriction
68
+ journal = self.env["account.journal"].create(
69
+ {
70
+ "name": "Test Journal No Company VERI*FACTU",
71
+ "type": "sale",
72
+ "code": "TJN",
73
+ "company_id": self.company.id,
74
+ "verifactu_enabled": True,
75
+ "restrict_mode_hash_table": False,
76
+ }
77
+ )
78
+ self.assertFalse(journal.restrict_mode_hash_table)
@@ -0,0 +1,93 @@
1
+ # Copyright 2025 Process Control - Jorge Luis López
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3
+
4
+ from .common import TestVerifactuCommon
5
+
6
+
7
+ class TestAccountMoveReversal(TestVerifactuCommon):
8
+ def test_reverse_moves_sets_refund_type(self):
9
+ """Test that reversing moves sets correct VERI*FACTU refund type"""
10
+ # Create and post invoice
11
+ self.invoice.action_post()
12
+
13
+ # Create reversal wizard
14
+ reversal_wizard = (
15
+ self.env["account.move.reversal"]
16
+ .with_context(
17
+ **{
18
+ "active_model": "account.move",
19
+ "active_ids": self.invoice.ids,
20
+ }
21
+ )
22
+ .create(
23
+ {
24
+ "reason": "Test reversal",
25
+ "refund_method": "refund",
26
+ "journal_id": self.invoice.journal_id.id,
27
+ }
28
+ )
29
+ )
30
+
31
+ # Execute reversal
32
+ result = reversal_wizard.reverse_moves()
33
+
34
+ # Get the credit note created
35
+ credit_note = self.invoice.reversal_move_id
36
+ self.assertTrue(credit_note)
37
+ self.assertEqual(credit_note.move_type, "out_refund")
38
+ self.assertEqual(credit_note.verifactu_refund_type, "I")
39
+
40
+ # Verify result contains the correct action
41
+ self.assertIn("res_id", result)
42
+
43
+ def test_reverse_moves_only_affects_customer_invoices(self):
44
+ """Test that reversal only affects customer invoices, not vendor bills"""
45
+ # Create vendor bill
46
+ vendor_bill = self.env["account.move"].create(
47
+ {
48
+ "move_type": "in_invoice",
49
+ "partner_id": self.partner.id,
50
+ "company_id": self.company.id,
51
+ "invoice_date": "2024-01-01",
52
+ "invoice_line_ids": [
53
+ (
54
+ 0,
55
+ 0,
56
+ {
57
+ "product_id": self.product.id,
58
+ "quantity": 1,
59
+ "price_unit": 100,
60
+ },
61
+ )
62
+ ],
63
+ }
64
+ )
65
+ vendor_bill.action_post()
66
+
67
+ # Create reversal wizard for vendor bill
68
+ reversal_wizard = (
69
+ self.env["account.move.reversal"]
70
+ .with_context(
71
+ **{
72
+ "active_model": "account.move",
73
+ "active_ids": vendor_bill.ids,
74
+ }
75
+ )
76
+ .create(
77
+ {
78
+ "reason": "Test vendor bill reversal",
79
+ "refund_method": "refund",
80
+ "journal_id": vendor_bill.journal_id.id,
81
+ }
82
+ )
83
+ )
84
+
85
+ # Execute reversal
86
+ reversal_wizard.reverse_moves()
87
+
88
+ # Vendor bill reversal should not have verifactu_refund_type set
89
+ credit_note = vendor_bill.reversal_move_id
90
+ self.assertTrue(credit_note)
91
+ self.assertEqual(credit_note.move_type, "in_refund")
92
+ # verifactu_refund_type should remain False for vendor bills
93
+ self.assertFalse(credit_note.verifactu_refund_type)
@@ -0,0 +1,48 @@
1
+ # Copyright 2025 Process Control - Jorge Luis López
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3
+
4
+ from .common import TestVerifactuCommon
5
+
6
+
7
+ class TestResPartner(TestVerifactuCommon):
8
+ def test_partner_aeat_sending_enabled_computation(self):
9
+ """Test computation of aeat_sending_enabled field"""
10
+ # Test with company VERI*FACTU enabled
11
+ partner_with_company = self.env["res.partner"].create(
12
+ {
13
+ "name": "Test Partner with Company",
14
+ "company_id": self.company.id,
15
+ }
16
+ )
17
+ partner_with_company._compute_aeat_sending_enabled()
18
+ self.assertTrue(partner_with_company.aeat_sending_enabled)
19
+ self.assertTrue(partner_with_company.verifactu_enabled)
20
+
21
+ # Test with company VERI*FACTU disabled
22
+ company_disabled = self.env["res.company"].create(
23
+ {
24
+ "name": "Test Company Disabled",
25
+ "verifactu_enabled": False,
26
+ }
27
+ )
28
+ partner_disabled = self.env["res.partner"].create(
29
+ {
30
+ "name": "Test Partner Disabled",
31
+ "company_id": company_disabled.id,
32
+ }
33
+ )
34
+ partner_disabled._compute_aeat_sending_enabled()
35
+ self.assertFalse(partner_disabled.aeat_sending_enabled)
36
+ self.assertFalse(partner_disabled.verifactu_enabled)
37
+
38
+ # Test partner without specific company but with global VERI*FACTU enabled
39
+ partner_no_company = self.env["res.partner"].create(
40
+ {
41
+ "name": "Test Partner No Company",
42
+ "company_id": False,
43
+ }
44
+ )
45
+ # Test when at least one company has VERI*FACTU enabled
46
+ partner_no_company._compute_aeat_sending_enabled()
47
+ self.assertTrue(partner_no_company.aeat_sending_enabled)
48
+ self.assertTrue(partner_no_company.verifactu_enabled)