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.
- odoo/addons/l10n_es_verifactu_oca/README.rst +195 -0
- odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
- odoo/addons/l10n_es_verifactu_oca/__manifest__.py +50 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +13 -0
- odoo/addons/l10n_es_verifactu_oca/data/l10n.es.aeat.map.tax.line.tax.csv +50 -0
- odoo/addons/l10n_es_verifactu_oca/data/mail_activity_data.xml +11 -0
- odoo/addons/l10n_es_verifactu_oca/data/neutralize.sql +2 -0
- odoo/addons/l10n_es_verifactu_oca/data/template/account.fiscal.position-es_common.csv +27 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.csv +2 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.line.csv +8 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +205 -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/es.po +1682 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1640 -0
- odoo/addons/l10n_es_verifactu_oca/migrations/18.0.1.1.0/pre-migration.py +25 -0
- odoo/addons/l10n_es_verifactu_oca/models/__init__.py +15 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_chart_template.py +17 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +34 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_move.py +631 -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 +26 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +37 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +398 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +116 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +47 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +68 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +485 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +26 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.md +27 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.md +18 -0
- odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.md +1 -0
- odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.md +6 -0
- odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.md +30 -0
- odoo/addons/l10n_es_verifactu_oca/readme/USAGE.md +3 -0
- odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +23 -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 +551 -0
- odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +2 -0
- odoo/addons/l10n_es_verifactu_oca/tests/common.py +281 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_accepted_with_errors.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_incorrect.json +37 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_with_errors.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_correct.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_duplicated.json +43 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_incorrect.json +37 -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 +506 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +348 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +29 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +22 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +237 -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 +53 -0
- odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +45 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +46 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +134 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +127 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +16 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +54 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +43 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +2 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard.py +24 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard_view.xml +35 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/METADATA +213 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/RECORD +78 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/WHEEL +5 -0
- 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
|
+
)
|