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.
- odoo/addons/l10n_es_verifactu_oca/README.rst +154 -0
- odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
- odoo/addons/l10n_es_verifactu_oca/__manifest__.py +48 -0
- odoo/addons/l10n_es_verifactu_oca/data/account_fiscal_position_template_data.xml +129 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +14 -0
- odoo/addons/l10n_es_verifactu_oca/data/mail_activity_data.xml +11 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_map_data.xml +120 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +207 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
- odoo/addons/l10n_es_verifactu_oca/hooks.py +43 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/ca.po +1630 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/ca_ES.po +1599 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1640 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1673 -0
- odoo/addons/l10n_es_verifactu_oca/models/__init__.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +40 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position_template.py +18 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_move.py +556 -0
- odoo/addons/l10n_es_verifactu_oca/models/aeat_mixin.py +163 -0
- odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
- odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
- odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +33 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +30 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +401 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +121 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +35 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +66 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +449 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +24 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.rst +18 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.rst +19 -0
- odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.rst +1 -0
- odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.rst +4 -0
- odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.rst +15 -0
- odoo/addons/l10n_es_verifactu_oca/readme/USAGE.rst +1 -0
- odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +22 -0
- odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
- odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
- odoo/addons/l10n_es_verifactu_oca/static/description/index.html +508 -0
- odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +5 -0
- odoo/addons/l10n_es_verifactu_oca/tests/common.py +304 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_1.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_2.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +451 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_account_journal.py +78 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_account_move_reversal.py +93 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_res_partner.py +48 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +350 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +30 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +28 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +219 -0
- odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
- odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +55 -0
- odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
- odoo/addons/l10n_es_verifactu_oca/views/res_partner_view.xml +27 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +47 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +48 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +149 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +124 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +20 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +53 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +42 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +1 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
- odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/METADATA +171 -0
- odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/RECORD +74 -0
- odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/WHEEL +5 -0
- 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)
|