odoo-addon-l10n-es-verifactu-oca 18.0.1.2.1__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.
Files changed (78) hide show
  1. odoo/addons/l10n_es_verifactu_oca/README.rst +195 -0
  2. odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
  3. odoo/addons/l10n_es_verifactu_oca/__manifest__.py +50 -0
  4. odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
  5. odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +13 -0
  6. odoo/addons/l10n_es_verifactu_oca/data/l10n.es.aeat.map.tax.line.tax.csv +50 -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/template/account.fiscal.position-es_common.csv +27 -0
  10. odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.csv +2 -0
  11. odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.line.csv +8 -0
  12. odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +205 -0
  13. odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
  14. odoo/addons/l10n_es_verifactu_oca/hooks.py +43 -0
  15. odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1682 -0
  16. odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1640 -0
  17. odoo/addons/l10n_es_verifactu_oca/migrations/18.0.1.1.0/pre-migration.py +25 -0
  18. odoo/addons/l10n_es_verifactu_oca/models/__init__.py +15 -0
  19. odoo/addons/l10n_es_verifactu_oca/models/account_chart_template.py +17 -0
  20. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +34 -0
  21. odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
  22. odoo/addons/l10n_es_verifactu_oca/models/account_move.py +631 -0
  23. odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
  24. odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
  25. odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +26 -0
  26. odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +37 -0
  27. odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
  28. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +398 -0
  29. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +116 -0
  30. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +47 -0
  31. odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +68 -0
  32. odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +485 -0
  33. odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +26 -0
  34. odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.md +27 -0
  35. odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.md +18 -0
  36. odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.md +1 -0
  37. odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.md +6 -0
  38. odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.md +30 -0
  39. odoo/addons/l10n_es_verifactu_oca/readme/USAGE.md +3 -0
  40. odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +23 -0
  41. odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
  42. odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
  43. odoo/addons/l10n_es_verifactu_oca/static/description/index.html +551 -0
  44. odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +2 -0
  45. odoo/addons/l10n_es_verifactu_oca/tests/common.py +281 -0
  46. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_accepted_with_errors.json +35 -0
  47. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel.json +35 -0
  48. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_incorrect.json +37 -0
  49. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_with_errors.json +35 -0
  50. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_correct.json +35 -0
  51. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_duplicated.json +43 -0
  52. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_incorrect.json +37 -0
  53. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
  54. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
  55. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
  56. odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +506 -0
  57. odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +348 -0
  58. odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +29 -0
  59. odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +22 -0
  60. odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +237 -0
  61. odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
  62. odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +53 -0
  63. odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
  64. odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +45 -0
  65. odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +46 -0
  66. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +134 -0
  67. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +127 -0
  68. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +16 -0
  69. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +54 -0
  70. odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +43 -0
  71. odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +2 -0
  72. odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
  73. odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard.py +24 -0
  74. odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard_view.xml +35 -0
  75. odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/METADATA +213 -0
  76. odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/RECORD +78 -0
  77. odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/WHEEL +5 -0
  78. odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,66 @@
1
+ {
2
+ "RegistroAlta": {
3
+ "IDVersion": 1.0,
4
+ "IDFactura": {
5
+ "IDEmisorFactura": "G87846952",
6
+ "NumSerieFactura": "TEST002",
7
+ "FechaExpedicionFactura": "01-01-2026"
8
+ },
9
+ "NombreRazonEmisor": "Spanish test company",
10
+ "TipoFactura": "R1",
11
+ "TipoRectificativa": "I",
12
+ "DescripcionOperacion": "/",
13
+ "Destinatarios": {
14
+ "IDDestinatario": {
15
+ "NombreRazon": "Test partner",
16
+ "NIF": "89890001K"
17
+ }
18
+ },
19
+ "Desglose": {
20
+ "DetalleDesglose": [
21
+ {
22
+ "Impuesto": "01",
23
+ "ClaveRegimen": "01",
24
+ "CalificacionOperacion": "S1",
25
+ "TipoImpositivo": "10.0",
26
+ "BaseImponibleOimporteNoSujeto": -200.0,
27
+ "CuotaRepercutida": -20.0
28
+ },
29
+ {
30
+ "Impuesto": "01",
31
+ "ClaveRegimen": "01",
32
+ "CalificacionOperacion": "S1",
33
+ "TipoImpositivo": "21.0",
34
+ "BaseImponibleOimporteNoSujeto": -200.0,
35
+ "CuotaRepercutida": -42.0
36
+ }
37
+ ]
38
+ },
39
+ "CuotaTotal": -62.0,
40
+ "ImporteTotal": -462.0,
41
+ "Encadenamiento": {
42
+ "RegistroAnterior": {
43
+ "FechaExpedicionFactura": "01-01-2026",
44
+ "Huella": "27656D2A18B1F7E6823B7C134F0AFA3B979D0DCDB37846593F9E4E089F9383B0",
45
+ "IDEmisorFactura": "G87846952",
46
+ "NumSerieFactura": "TEST001"
47
+ }
48
+ },
49
+ "FacturasRectificadas": [],
50
+ "SistemaInformatico": {
51
+ "NombreRazon": "Odoo Developer",
52
+ "NIF": "A12345674",
53
+ "NombreSistemaInformatico": "odoo",
54
+ "IdSistemaInformatico": "11",
55
+ "Version": "1.0",
56
+ "NumeroInstalacion": 1,
57
+ "TipoUsoPosibleSoloVerifactu": "S",
58
+ "TipoUsoPosibleMultiOT": "S",
59
+ "IndicadorMultiplesOT": "N",
60
+ "IDOtro": {
61
+ "IDType": "",
62
+ "ID": ""
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,506 @@
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.tools.misc import file_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: E501
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 = f"account.{self.company.id}_account_tax_template_{tax}"
55
+ taxes += self.env.ref(xml_id)
56
+ tax_names.append(tax)
57
+ vals.append({"price_unit": line[0], "taxes": taxes})
58
+ return self._compare_verifactu_dict(
59
+ "verifactu_{}_{}_dict.json".format(inv_type, "_".join(tax_names)),
60
+ name,
61
+ inv_type,
62
+ vals,
63
+ extra_vals=extra_vals,
64
+ module=module,
65
+ )
66
+
67
+ def _compare_verifactu_dict(
68
+ self, json_file, name, inv_type, lines, extra_vals=None, module=None
69
+ ):
70
+ """Helper method for creating an invoice according arguments, and
71
+ comparing the expected verifactu dict with .
72
+ """
73
+ module = module or "l10n_es_verifactu_oca"
74
+ vals = {
75
+ "name": name,
76
+ "partner_id": self.partner.id,
77
+ "invoice_date": "2026-01-01",
78
+ "move_type": inv_type,
79
+ "invoice_line_ids": [],
80
+ }
81
+ for line in lines:
82
+ vals["invoice_line_ids"].append(
83
+ Command.create(
84
+ {
85
+ "product_id": self.product.id,
86
+ "account_id": self.account_expense.id,
87
+ "name": "Test line",
88
+ "price_unit": line["price_unit"],
89
+ "quantity": 1,
90
+ "tax_ids": [(6, 0, line["taxes"].ids)],
91
+ },
92
+ )
93
+ )
94
+ if extra_vals:
95
+ vals.update(extra_vals)
96
+ invoice = self.env["account.move"].create(vals)
97
+ self._activate_certificate(self.certificate_password)
98
+ first_now = datetime(2026, 1, 1, 19, 20, 30)
99
+ with (
100
+ patch.object(self.env.cr, "now", lambda: first_now),
101
+ freeze_time(first_now),
102
+ ):
103
+ invoice.action_post()
104
+ result_dict = invoice._get_verifactu_invoice_dict()
105
+ result_dict["RegistroAlta"].pop("FechaHoraHusoGenRegistro")
106
+ result_dict["RegistroAlta"].pop("TipoHuella")
107
+ result_dict["RegistroAlta"].pop("Huella")
108
+ path = file_path(f"{module}/tests/json/{json_file}")
109
+ if not path:
110
+ raise Exception(f"Incorrect JSON file: {json_file}")
111
+ with open(path) as f:
112
+ expected_dict = json.loads(f.read())
113
+ self.assertEqual(expected_dict, result_dict)
114
+ entry = invoice.last_verifactu_invoice_entry_id
115
+ # Verify integration workflow
116
+ self.assertTrue(entry, "Invoice should have verifactu entry")
117
+ self.assertTrue(entry.aeat_json_data, "Should have JSON data")
118
+ return invoice
119
+
120
+ def test_get_verifactu_invoice_data(self):
121
+ mapping = [
122
+ (
123
+ "TEST001",
124
+ "out_invoice",
125
+ [(100, ["s_iva10b"]), (200, ["s_iva21s"])],
126
+ {
127
+ "fiscal_position_id": self.fp_nacional.id,
128
+ "verifactu_registration_key": self.fp_registration_key_01.id,
129
+ "verifactu_registration_date": "2026-01-01 19:20:30",
130
+ },
131
+ ),
132
+ (
133
+ "TEST002",
134
+ "out_refund",
135
+ [(100, ["s_iva10b"]), (100, ["s_iva10b"]), (200, ["s_iva21s"])],
136
+ {
137
+ "fiscal_position_id": self.fp_nacional.id,
138
+ "verifactu_registration_key": self.fp_registration_key_01.id,
139
+ "verifactu_registration_date": "2026-01-01 19:20:30",
140
+ },
141
+ ),
142
+ (
143
+ "TEST003",
144
+ "out_invoice",
145
+ [(200, ["s_iva21s", "s_req52"])],
146
+ {
147
+ "fiscal_position_id": self.fp_recargo.id,
148
+ "verifactu_registration_key": self.fp_registration_key_01.id,
149
+ "verifactu_registration_date": "2026-01-01 19:20:30",
150
+ },
151
+ ),
152
+ ]
153
+ for name, inv_type, lines, extra_vals in mapping:
154
+ self._create_and_test_invoice_verifactu_dict(
155
+ name, inv_type, lines, extra_vals
156
+ )
157
+ return
158
+
159
+ def test_verifactu_start_date(self):
160
+ self.company.verifactu_start_date = "2018-01-01"
161
+ invoice1 = self.invoice.copy({"invoice_date": "2019-01-01"})
162
+ self.assertTrue(invoice1.verifactu_enabled)
163
+ invoice2 = self.invoice.copy({"invoice_date": "2017-01-01"})
164
+ invoice2.invoice_date = "2017-01-01"
165
+ self.assertFalse(invoice2.verifactu_enabled)
166
+ self.company.verifactu_start_date = False
167
+ self.assertTrue(invoice2.verifactu_enabled)
168
+
169
+
170
+ class TestL10nEsAeatVerifactuQR(TestVerifactuCommon):
171
+ def _get_required_qr_params(self):
172
+ """Helper to generate the required QR code parameters."""
173
+ return {
174
+ "nif": self.invoice.company_id.partner_id._parse_aeat_vat_info()[2],
175
+ "numserie": self.invoice.name,
176
+ "fecha": self.invoice._get_verifactu_date(self.invoice.invoice_date),
177
+ "importe": f"{self.invoice.amount_total:.2f}", # noqa
178
+ }
179
+
180
+ def test_verifactu_qr_generation(self):
181
+ """
182
+ Test the generation of the QR code image for the invoice.
183
+ """
184
+ self._activate_certificate(self.certificate_password)
185
+ self.invoice.action_post()
186
+ qr_code = self.invoice.verifactu_qr
187
+ self.assertTrue(qr_code, "QR code should be generated for the invoice.")
188
+ self.assertIsInstance(qr_code, bytes, "QR code should be in bytes format.")
189
+
190
+ def test_verifactu_qr_url_format(self):
191
+ """
192
+ Test the format of the generated QR URL to ensure it meets expected criteria.
193
+ """
194
+ self._activate_certificate(self.certificate_password)
195
+ self.invoice.action_post()
196
+ qr_url = self.invoice.verifactu_qr_url
197
+ self.assertTrue(qr_url, "QR URL should be generated for the invoice.")
198
+ test_url = self.env.ref(
199
+ "l10n_es_aeat.aeat_tax_agency_spain"
200
+ ).verifactu_qr_base_url_test_address
201
+ self.assertTrue(test_url, "Test URL should not be empty.")
202
+ parsed_url = urlparse(qr_url)
203
+ actual_params = parse_qs(parsed_url.query)
204
+ expected_params = self._get_required_qr_params()
205
+ for key, expected_value in expected_params.items():
206
+ self.assertIn(
207
+ key, actual_params, f"QR URL should contain the parameter: {key}"
208
+ )
209
+ self.assertEqual(
210
+ actual_params[key][0],
211
+ str(expected_value),
212
+ f"QR URL parameter '{key}' should have value '{expected_value}', "
213
+ "got '{actual_params[key][0]}' instead.",
214
+ )
215
+
216
+ def test_verifactu_qr_code_generation_on_draft(self):
217
+ """
218
+ Ensure that the QR code is not generated for invoices in draft state.
219
+ """
220
+ qr_code = self.invoice.verifactu_qr
221
+ self.assertFalse(qr_code, "QR code should not be generated for draft invoices.")
222
+
223
+ def test_verifactu_qr_code_after_update(self):
224
+ """
225
+ Test that the QR code is regenerated if the invoice details are updated.
226
+ """
227
+ self._activate_certificate(self.certificate_password)
228
+ self.invoice.action_post()
229
+ original_qr_code = self.invoice.verifactu_qr
230
+ with self.assertRaises(UserError):
231
+ self.invoice.button_cancel()
232
+ self.invoice.button_draft()
233
+ self.invoice.write(
234
+ {
235
+ "invoice_line_ids": [
236
+ Command.create(
237
+ {
238
+ "product_id": self.product.id,
239
+ "account_id": self.account_expense.id,
240
+ "name": "Updated line",
241
+ "price_unit": 200,
242
+ "quantity": 1,
243
+ },
244
+ )
245
+ ]
246
+ }
247
+ )
248
+ self.invoice.action_post()
249
+ self.invoice.invalidate_model(["verifactu_qr_url", "verifactu_qr"])
250
+ updated_qr_code = self.invoice.verifactu_qr
251
+ self.assertNotEqual(
252
+ original_qr_code,
253
+ updated_qr_code,
254
+ "QR code should be regenerated after invoice update.",
255
+ )
256
+
257
+ def test_send_invoices_to_verifactu(self):
258
+ self._activate_certificate(self.certificate_password)
259
+ self.invoice.action_post()
260
+ with patch(
261
+ "odoo.addons.l10n_es_verifactu_oca.models."
262
+ "verifactu_invoice_entry.VerifactuInvoiceEntry._connect_verifactu"
263
+ ) as mock_connect:
264
+ mock_service = MagicMock()
265
+ module = "l10n_es_verifactu_oca"
266
+ json_file = "verifactu_mocked_response_correct.json"
267
+ path = file_path(f"{module}/tests/json/{json_file}")
268
+ if not path:
269
+ raise Exception(f"Incorrect JSON file: {json_file}")
270
+ with open(path) as f:
271
+ response_dict = json.loads(f.read())
272
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
273
+ mock_connect.return_value = mock_service
274
+ # Execute the cron job to send the invoice to VERI*FACTU
275
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
276
+ self.assertEqual(
277
+ self.invoice.aeat_state,
278
+ "sent",
279
+ "Invoice should be marked as sent after VERI*FACTU processing.",
280
+ )
281
+ self.assertEqual(
282
+ self.invoice.verifactu_csv,
283
+ "A-Y23JP3582934",
284
+ "CSV should be generated correctly after sending to VERI*FACTU.",
285
+ )
286
+
287
+ def mock_test(self, mock_connect, json_file):
288
+ mock_service = MagicMock()
289
+ module = "l10n_es_verifactu_oca"
290
+ path = file_path(f"{module}/tests/json/{json_file}")
291
+ if not path:
292
+ raise Exception(f"Incorrect JSON file: {json_file}")
293
+ with open(path) as f:
294
+ response_dict = json.loads(f.read())
295
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
296
+ mock_connect.return_value = mock_service
297
+ # Execute the cron job to send the invoice to VERI*FACTU
298
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
299
+
300
+ def test_send_invoices_to_verifactu_with_incorrect_response(self):
301
+ self._activate_certificate(self.certificate_password)
302
+ self.invoice.action_post()
303
+ with patch(
304
+ "odoo.addons.l10n_es_verifactu_oca.models."
305
+ "verifactu_invoice_entry.VerifactuInvoiceEntry._connect_verifactu"
306
+ ) as mock_connect:
307
+ json_file = "verifactu_mocked_response_incorrect.json"
308
+ self.mock_test(mock_connect, json_file)
309
+ self.assertEqual(
310
+ self.invoice.aeat_state,
311
+ "incorrect",
312
+ "Invoice should be marked as incorrect after VERI*FACTU processing.",
313
+ )
314
+ self.assertEqual(
315
+ self.invoice.aeat_send_failed,
316
+ True,
317
+ "Invoice send be marked as failed after VERI*FACTU processing.",
318
+ )
319
+
320
+ def test_send_invoices_to_verifactu_duplicated(self):
321
+ self._activate_certificate(self.certificate_password)
322
+ self.invoice.action_post()
323
+ with patch(
324
+ "odoo.addons.l10n_es_verifactu_oca.models."
325
+ "verifactu_invoice_entry.VerifactuInvoiceEntry._connect_verifactu"
326
+ ) as mock_connect:
327
+ json_file = "verifactu_mocked_response_correct.json"
328
+ self.mock_test(mock_connect, json_file)
329
+ self.assertEqual(
330
+ self.invoice.aeat_state,
331
+ "sent",
332
+ "Invoice should be marked as sent after VERI*FACTU processing.",
333
+ )
334
+ # now we send the same invoice again
335
+ # we need to truncate the aeat_state as if the previous response was
336
+ # incorrect to force a new send a get the duplicated response
337
+ self.invoice.aeat_state = "incorrect"
338
+ self.invoice.resend_verifactu()
339
+ json_file = "verifactu_mocked_response_duplicated.json"
340
+ self.mock_test(mock_connect, json_file)
341
+ self.assertEqual(
342
+ self.invoice.aeat_state,
343
+ "incorrect",
344
+ "Invoice should be marked as incorrect after VERI*FACTU processing.",
345
+ )
346
+
347
+ def test_cancel_invoices_to_verifactu(self):
348
+ self._activate_certificate(self.certificate_password)
349
+ self.invoice.action_post()
350
+ with patch(
351
+ "odoo.addons.l10n_es_verifactu_oca.models."
352
+ "verifactu_invoice_entry.VerifactuInvoiceEntry._connect_verifactu"
353
+ ) as mock_connect:
354
+ json_file = "verifactu_mocked_response_correct.json"
355
+ self.mock_test(mock_connect, json_file)
356
+ self.assertEqual(
357
+ self.invoice.aeat_state,
358
+ "sent",
359
+ "Invoice should be marked as sent after VERI*FACTU processing.",
360
+ )
361
+ # now send the cancellation to verifactu w/ incorrect cancellation response
362
+ wiz = self.env["verifactu.cancel.invoice.wizard"].create(
363
+ {"invoice_id": self.invoice.id, "cancel_reason": "Test Cancel Reason"}
364
+ )
365
+ wiz.cancel_invoice_in_verifactu()
366
+ self.assertEqual(
367
+ self.invoice.state, "cancel", "Invoice should be in cancel state"
368
+ )
369
+ self.assertEqual(
370
+ self.invoice.verifactu_cancel_reason,
371
+ "Test Cancel Reason",
372
+ "Invoice cancel reason should be Test Cancel Reason",
373
+ )
374
+ json_file = "verifactu_mocked_response_cancel_incorrect.json"
375
+ self.mock_test(mock_connect, json_file)
376
+ self.assertEqual(
377
+ self.invoice.aeat_state,
378
+ "cancel_incorrect",
379
+ "Invoice should be marked as incorrect cancellation"
380
+ "after VERI*FACTU processing.",
381
+ )
382
+ # now send the cancellation to verifactu w/ cancellation w/ errors response
383
+ self.invoice.cancel_verifactu()
384
+ json_file = "verifactu_mocked_response_cancel_with_errors.json"
385
+ self.mock_test(mock_connect, json_file)
386
+ self.assertEqual(
387
+ self.invoice.aeat_state,
388
+ "cancel_w_errors",
389
+ "Invoice should be marked as cancelled with errors"
390
+ "after VERI*FACTU processing.",
391
+ )
392
+ # finally send cancellation to verifactu w/ correct cancellation response
393
+ self.invoice.cancel_verifactu()
394
+ json_file = "verifactu_mocked_response_cancel.json"
395
+ self.mock_test(mock_connect, json_file)
396
+ self.assertEqual(
397
+ self.invoice.aeat_state,
398
+ "cancel",
399
+ "Invoice should be marked as cancelled after VERI*FACTU processing.",
400
+ )
401
+
402
+
403
+ class TestVerifactuSendResponse(TestVerifactuCommon):
404
+ def test_create_activity_on_exception(self):
405
+ """
406
+ Creates an activity whenever the connection with VERI*FACTU
407
+ is not possible.
408
+ """
409
+ MailActivity = self.env["mail.activity"]
410
+ ActivityType = self.env.ref(
411
+ "l10n_es_verifactu_oca.mail_activity_data_exception"
412
+ )
413
+ # Send an invoice without a certificate
414
+ self.invoice.action_post()
415
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
416
+ self.assertEqual(self.invoice.aeat_state, "not_sent")
417
+ activity_1 = MailActivity.search(
418
+ [
419
+ ("activity_type_id", "=", ActivityType.id),
420
+ ("res_model", "=", "verifactu.invoice.entry.response"),
421
+ ]
422
+ )
423
+ self.assertTrue(activity_1, "An exception activity should have been created")
424
+ self.invoice.resend_verifactu()
425
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
426
+ activity_2 = MailActivity.search(
427
+ [
428
+ ("activity_type_id", "=", ActivityType.id),
429
+ ("res_model", "=", "verifactu.invoice.entry.response"),
430
+ ]
431
+ )
432
+ self.assertEqual(
433
+ len(activity_1),
434
+ len(activity_2),
435
+ "There should be only one exception activity created",
436
+ )
437
+ # Activate certificate and re-run the cron
438
+ self._activate_certificate(self.certificate_password)
439
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
440
+ activity_done = (
441
+ self.env["mail.activity"]
442
+ .with_context(active_test=False)
443
+ .search(
444
+ [
445
+ ("activity_type_id", "=", ActivityType.id),
446
+ ("res_model", "=", "verifactu.invoice.entry.response"),
447
+ ]
448
+ )
449
+ )
450
+ # todo: fix this, it's not activity_done.has_recommended_activites,
451
+ # should check if it's not visible anymore to the user
452
+ self.assertFalse(
453
+ activity_done.has_recommended_activities,
454
+ "The exception activity should not appear.",
455
+ )
456
+
457
+ def mock_verifactu_response(self, error_code, description):
458
+ """Recreates a verifactu response"""
459
+ return {
460
+ "CSV": "dummy-csv",
461
+ "RespuestaLinea": [
462
+ {
463
+ "IDFactura": {
464
+ "NumSerieFactura": self.invoice.name,
465
+ },
466
+ "EstadoRegistro": "AceptadoConErrores",
467
+ "CodigoErrorRegistro": error_code,
468
+ "DescripcionErrorRegistro": description,
469
+ }
470
+ ],
471
+ }
472
+
473
+ @patch(
474
+ "odoo.addons.l10n_es_verifactu_oca.models.verifactu_invoice_entry."
475
+ "VerifactuInvoiceEntry._connect_verifactu"
476
+ )
477
+ def test_create_send_activity(self, mock_connect):
478
+ """
479
+ Create an activity whenever the response from VERI*FACTU indicates
480
+ that incorrect invoices have been sent
481
+ """
482
+ MailActivity = self.env["mail.activity"]
483
+ ActivityType = self.env.ref("mail.mail_activity_data_warning")
484
+ mock_service = MagicMock()
485
+ module = "l10n_es_verifactu_oca"
486
+ json_file = "verifactu_mocked_response_accepted_with_errors.json"
487
+ path = file_path(f"{module}/tests/json/{json_file}")
488
+ if not path:
489
+ raise Exception(f"Incorrect JSON file: {json_file}")
490
+ with open(path) as f:
491
+ response_dict = json.loads(f.read())
492
+ mock_service.RegFactuSistemaFacturacion.return_value = response_dict
493
+ mock_connect.return_value = mock_service
494
+ self.invoice.action_post()
495
+ self.env["verifactu.invoice.entry"]._cron_send_documents_to_verifactu()
496
+ activity = MailActivity.search(
497
+ [
498
+ ("activity_type_id", "=", ActivityType.id),
499
+ ("res_model", "=", "verifactu.invoice.entry.response"),
500
+ ("summary", "=", "Check incorrect invoices from VERI*FACTU"),
501
+ ]
502
+ )
503
+ self.assertTrue(
504
+ activity,
505
+ "A warning activity should be created for 'AceptadoConErrores' response",
506
+ )