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,401 @@
|
|
|
1
|
+
# Copyright 2025 ForgeFlow S.L.
|
|
2
|
+
# Copyright 2025 Tecnativa - Pedro M. Baeza
|
|
3
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from requests import Session
|
|
9
|
+
from zeep import Client, Settings
|
|
10
|
+
from zeep.plugins import HistoryPlugin
|
|
11
|
+
from zeep.transports import Transport
|
|
12
|
+
|
|
13
|
+
from odoo import _, api, fields, models
|
|
14
|
+
from odoo.exceptions import UserError
|
|
15
|
+
from odoo.tools import split_every
|
|
16
|
+
|
|
17
|
+
_logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
VERIFACTU_SEND_STATES = [
|
|
20
|
+
("not_sent", "Not sent"),
|
|
21
|
+
("correct", "Sent and Correct"),
|
|
22
|
+
("incorrect", "Sent and Incorrect"),
|
|
23
|
+
("accepted_with_errors", "Sent and accepted with errors"),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
VERIFACTU_STATE_MAPPING = {
|
|
27
|
+
"Correcto": "correct",
|
|
28
|
+
"Incorrecto": "incorrect",
|
|
29
|
+
"AceptadoConErrores": "accepted_with_errors",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VerifactuInvoiceEntry(models.Model):
|
|
34
|
+
_name = "verifactu.invoice.entry"
|
|
35
|
+
_description = "VERI*FACTU invoice entry"
|
|
36
|
+
_order = "id desc"
|
|
37
|
+
_rec_name = "document_hash"
|
|
38
|
+
|
|
39
|
+
verifactu_chaining_id = fields.Many2one(
|
|
40
|
+
"verifactu.chaining", string="Chaining", ondelete="restrict", required=True
|
|
41
|
+
)
|
|
42
|
+
model = fields.Char(readonly=True, required=True)
|
|
43
|
+
document_id = fields.Many2oneReference(
|
|
44
|
+
string="Document", model_field="model", readonly=True, index=True, required=True
|
|
45
|
+
)
|
|
46
|
+
document_name = fields.Char(readonly=True)
|
|
47
|
+
previous_invoice_entry_id = fields.Many2one(
|
|
48
|
+
"verifactu.invoice.entry", string="Previous Invoice Entry", readonly=True
|
|
49
|
+
)
|
|
50
|
+
company_id = fields.Many2one(
|
|
51
|
+
"res.company", string="Company", required=True, readonly=True
|
|
52
|
+
)
|
|
53
|
+
document_hash = fields.Char(required=True, readonly=True)
|
|
54
|
+
aeat_json_data = fields.Text(
|
|
55
|
+
string="AEAT JSON Data",
|
|
56
|
+
help="Generated JSON data to send to AEAT",
|
|
57
|
+
readonly=True,
|
|
58
|
+
)
|
|
59
|
+
send_state = fields.Selection(
|
|
60
|
+
selection=VERIFACTU_SEND_STATES,
|
|
61
|
+
compute="_compute_send_state",
|
|
62
|
+
default="not_sent",
|
|
63
|
+
readonly=True,
|
|
64
|
+
store=True,
|
|
65
|
+
copy=False,
|
|
66
|
+
help="Indicates the state of this document in relation with the "
|
|
67
|
+
"presentation to VERI*FACTU.",
|
|
68
|
+
)
|
|
69
|
+
send_attempt = fields.Integer(
|
|
70
|
+
default=0, help="Number of attempts to send this document."
|
|
71
|
+
)
|
|
72
|
+
response_line_ids = fields.One2many(
|
|
73
|
+
"verifactu.invoice.entry.response.line",
|
|
74
|
+
"entry_id",
|
|
75
|
+
string="Responses",
|
|
76
|
+
help="Responses from VERI*FACTU after sending the documents.",
|
|
77
|
+
)
|
|
78
|
+
last_error_code = fields.Char(compute="_compute_last_error_code", store=True)
|
|
79
|
+
previous_hash = fields.Char(
|
|
80
|
+
related="previous_invoice_entry_id.document_hash",
|
|
81
|
+
readonly=True,
|
|
82
|
+
string="Previous Hash",
|
|
83
|
+
)
|
|
84
|
+
entry_type = fields.Selection(
|
|
85
|
+
selection=[
|
|
86
|
+
("register", "Register"),
|
|
87
|
+
("modify", "Modify"),
|
|
88
|
+
("cancel", "Cancel"),
|
|
89
|
+
],
|
|
90
|
+
default="register",
|
|
91
|
+
required=True,
|
|
92
|
+
)
|
|
93
|
+
last_response_line_id = fields.Many2one(
|
|
94
|
+
"verifactu.invoice.entry.response.line",
|
|
95
|
+
string="Last Response Line",
|
|
96
|
+
readonly=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@api.depends("response_line_ids", "response_line_ids.send_state")
|
|
100
|
+
def _compute_send_state(self):
|
|
101
|
+
for rec in self:
|
|
102
|
+
rec.send_state = "not_sent"
|
|
103
|
+
last_response = rec.last_response_line_id
|
|
104
|
+
if last_response:
|
|
105
|
+
rec.send_state = last_response.send_state
|
|
106
|
+
|
|
107
|
+
@api.depends("response_line_ids", "response_line_ids.error_code")
|
|
108
|
+
def _compute_last_error_code(self):
|
|
109
|
+
"""Compute the last error code from the response lines."""
|
|
110
|
+
for rec in self:
|
|
111
|
+
if rec.last_response_line_id:
|
|
112
|
+
rec.last_error_code = rec.last_response_line_id.error_code
|
|
113
|
+
else:
|
|
114
|
+
rec.last_error_code = ""
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def document(self):
|
|
118
|
+
return self.env[self.model].browse(self.document_id).exists()
|
|
119
|
+
|
|
120
|
+
@api.model
|
|
121
|
+
def _cron_send_documents_to_verifactu(self):
|
|
122
|
+
batch_limit = self.env["verifactu.mixin"]._get_verifactu_batch()
|
|
123
|
+
for chaining in self.env["verifactu.chaining"].search([]):
|
|
124
|
+
self.env.cr.execute(
|
|
125
|
+
"""
|
|
126
|
+
SELECT id FROM verifactu_invoice_entry AS vsq
|
|
127
|
+
WHERE vsq.send_state in ('not_sent', 'incorrect')
|
|
128
|
+
AND vsq.verifactu_chaining_id = %s
|
|
129
|
+
ORDER BY id
|
|
130
|
+
FOR UPDATE NOWAIT
|
|
131
|
+
""",
|
|
132
|
+
[chaining.id],
|
|
133
|
+
)
|
|
134
|
+
entries_to_send_ids = [entry[0] for entry in self.env.cr.fetchall()]
|
|
135
|
+
for entries_batch_ids in split_every(batch_limit, entries_to_send_ids):
|
|
136
|
+
records_to_send = self.browse(entries_batch_ids)
|
|
137
|
+
send_date = fields.Datetime.now()
|
|
138
|
+
threshold_time = send_date - datetime.timedelta(seconds=240)
|
|
139
|
+
# Look for documents where we have to send as an incident
|
|
140
|
+
outdated_records = records_to_send.filtered(
|
|
141
|
+
lambda r: r.document.verifactu_registration_date < threshold_time
|
|
142
|
+
)
|
|
143
|
+
current_records = records_to_send - outdated_records
|
|
144
|
+
outdated_records.with_context(
|
|
145
|
+
verifactu_incident=True
|
|
146
|
+
)._send_documents_to_verifactu()
|
|
147
|
+
current_records._send_documents_to_verifactu()
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
def _get_verifactu_aeat_header(self):
|
|
151
|
+
"""Builds VERI*FACTU send header
|
|
152
|
+
|
|
153
|
+
:param tipo_comunicacion String 'A0': new reg, 'A1': modification
|
|
154
|
+
:param cancellation Bool True when the communitacion es for document
|
|
155
|
+
cancellation
|
|
156
|
+
:return Dict with header data depending on cancellation
|
|
157
|
+
"""
|
|
158
|
+
self.ensure_one()
|
|
159
|
+
if not self.company_id.vat:
|
|
160
|
+
raise UserError(
|
|
161
|
+
_("No VAT configured for the company '{}'").format(self.company_id.name)
|
|
162
|
+
)
|
|
163
|
+
header = {
|
|
164
|
+
"ObligadoEmision": {
|
|
165
|
+
"NombreRazon": self.company_id.name[0:120],
|
|
166
|
+
"NIF": self.company_id.partner_id._parse_aeat_vat_info()[2],
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
incident = self.env.context.get("verifactu_incident", False)
|
|
170
|
+
if incident:
|
|
171
|
+
header.update({"RemisionVoluntaria": {"Incidencia": "S"}})
|
|
172
|
+
return header
|
|
173
|
+
|
|
174
|
+
def _bind_verifactu_service(self, client, port_name, address=None):
|
|
175
|
+
self.ensure_one()
|
|
176
|
+
service = client._get_service("sfVerifactu")
|
|
177
|
+
port = client._get_port(service, port_name)
|
|
178
|
+
address = address or port.binding_options["address"]
|
|
179
|
+
return client.create_service(port.binding.name, address)
|
|
180
|
+
|
|
181
|
+
def _connect_verifactu_params_aeat(self):
|
|
182
|
+
self.ensure_one()
|
|
183
|
+
agency = self.company_id.tax_agency_id
|
|
184
|
+
if not agency:
|
|
185
|
+
# We use spanish agency by default to keep old behavior with
|
|
186
|
+
# ir.config parameters. In the future it might be good to reinforce
|
|
187
|
+
# to explicitly set a tax agency in the company by raising an error
|
|
188
|
+
# here.
|
|
189
|
+
agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
|
|
190
|
+
return agency._connect_params_verifactu(self.company_id)
|
|
191
|
+
|
|
192
|
+
def _connect_verifactu(self):
|
|
193
|
+
self.ensure_one()
|
|
194
|
+
public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates(
|
|
195
|
+
company=self.company_id
|
|
196
|
+
)
|
|
197
|
+
if not public_crt or not private_key:
|
|
198
|
+
raise UserError(
|
|
199
|
+
_("Please, configure the VERI*FACTU certificates for your company")
|
|
200
|
+
)
|
|
201
|
+
params = self._connect_verifactu_params_aeat()
|
|
202
|
+
session = Session()
|
|
203
|
+
session.cert = (public_crt, private_key)
|
|
204
|
+
transport = Transport(session=session)
|
|
205
|
+
history = HistoryPlugin()
|
|
206
|
+
settings = Settings(forbid_entities=False)
|
|
207
|
+
client = Client(
|
|
208
|
+
wsdl=params["wsdl"],
|
|
209
|
+
transport=transport,
|
|
210
|
+
plugins=[history],
|
|
211
|
+
settings=settings,
|
|
212
|
+
)
|
|
213
|
+
return self._bind_verifactu_service(
|
|
214
|
+
client, params["port_name"], params["address"]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _process_response_line_doc_vals(
|
|
218
|
+
self,
|
|
219
|
+
verifactu_response=False,
|
|
220
|
+
verifactu_response_line=False,
|
|
221
|
+
response_line=False,
|
|
222
|
+
previous_response_line=False,
|
|
223
|
+
header_sent=False,
|
|
224
|
+
):
|
|
225
|
+
estado_registro = verifactu_response_line["EstadoRegistro"]
|
|
226
|
+
doc_vals = {
|
|
227
|
+
"aeat_header_sent": json.dumps(header_sent, indent=4),
|
|
228
|
+
}
|
|
229
|
+
doc_vals["verifactu_return"] = verifactu_response_line
|
|
230
|
+
send_error = False
|
|
231
|
+
if hasattr(verifactu_response_line, "CodigoErrorRegistro"):
|
|
232
|
+
send_error = "{} | {}".format(
|
|
233
|
+
str(verifactu_response_line["CodigoErrorRegistro"]),
|
|
234
|
+
str(verifactu_response_line["DescripcionErrorRegistro"]),
|
|
235
|
+
)
|
|
236
|
+
# si ya ha devuelto previamente registro duplicado, parseamos el estado
|
|
237
|
+
# del registro duplicado para dejar la factura correcta o incorrecta
|
|
238
|
+
if (
|
|
239
|
+
verifactu_response_line["CodigoErrorRegistro"] == 3000
|
|
240
|
+
and previous_response_line
|
|
241
|
+
and (
|
|
242
|
+
previous_response_line.error_code == "3000"
|
|
243
|
+
and previous_response_line.send_state == "incorrect"
|
|
244
|
+
)
|
|
245
|
+
):
|
|
246
|
+
registroDuplicado = verifactu_response_line["RegistroDuplicado"]
|
|
247
|
+
estado_registro = registroDuplicado["EstadoRegistroDuplicado"]
|
|
248
|
+
# en duplicados devuelve Correcta en vez de Correcto...
|
|
249
|
+
if estado_registro == "Correcta":
|
|
250
|
+
estado_registro = "Correcto"
|
|
251
|
+
response_line.send_state = "correct"
|
|
252
|
+
elif registroDuplicado["CodigoErrorRegistro"]:
|
|
253
|
+
# en duplicados devuelve AceptadaConErrores en vez de AceptadoConErrores...
|
|
254
|
+
if estado_registro == "AceptadaConErrores":
|
|
255
|
+
estado_registro = "AceptadoConErrores"
|
|
256
|
+
response_line.send_state = "accepted_with_errors"
|
|
257
|
+
send_error = "{} | {}".format(
|
|
258
|
+
str(registroDuplicado["CodigoErrorRegistro"]),
|
|
259
|
+
str(registroDuplicado["DescripcionErrorRegistro"]),
|
|
260
|
+
)
|
|
261
|
+
if estado_registro == "Correcto":
|
|
262
|
+
doc_vals.update(
|
|
263
|
+
{
|
|
264
|
+
"aeat_state": "sent",
|
|
265
|
+
"verifactu_csv": verifactu_response["CSV"],
|
|
266
|
+
"aeat_send_failed": False,
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
elif estado_registro == "AceptadoConErrores":
|
|
270
|
+
doc_vals.update(
|
|
271
|
+
{
|
|
272
|
+
"aeat_state": "sent_w_errors",
|
|
273
|
+
"verifactu_csv": verifactu_response["CSV"],
|
|
274
|
+
"aeat_send_failed": True,
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
doc_vals["aeat_send_failed"] = True
|
|
279
|
+
doc_vals["aeat_send_error"] = send_error
|
|
280
|
+
if response_line.document_id:
|
|
281
|
+
response_line.document.write(doc_vals)
|
|
282
|
+
return doc_vals
|
|
283
|
+
|
|
284
|
+
def _send_documents_to_verifactu(self):
|
|
285
|
+
if not self:
|
|
286
|
+
return False
|
|
287
|
+
rec = self[0]
|
|
288
|
+
header = rec._get_verifactu_aeat_header()
|
|
289
|
+
registro_factura_list = []
|
|
290
|
+
create_exception = False
|
|
291
|
+
for rec in self:
|
|
292
|
+
rec.send_attempt += 1
|
|
293
|
+
if rec.document:
|
|
294
|
+
inv_dict = rec.document._get_verifactu_invoice_dict()
|
|
295
|
+
registro_factura_list.append(inv_dict)
|
|
296
|
+
try:
|
|
297
|
+
serv = rec._connect_verifactu()
|
|
298
|
+
res = serv.RegFactuSistemaFacturacion(header, registro_factura_list)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
_logger.error("Error sending documents to VERI*FACTU: %s", e)
|
|
301
|
+
res = {}
|
|
302
|
+
create_exception = True
|
|
303
|
+
response_name = ""
|
|
304
|
+
response = (
|
|
305
|
+
self.env["verifactu.invoice.entry.response"]
|
|
306
|
+
.sudo()
|
|
307
|
+
.create(
|
|
308
|
+
{
|
|
309
|
+
"header": json.dumps(header),
|
|
310
|
+
"name": response_name,
|
|
311
|
+
"invoice_data": json.dumps(registro_factura_list),
|
|
312
|
+
"response": res,
|
|
313
|
+
"verifactu_csv": "CSV" in res and res["CSV"] or _("-"),
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
response.complete_open_activity_on_exception()
|
|
318
|
+
if create_exception:
|
|
319
|
+
if not response.date_response:
|
|
320
|
+
response.date_response = fields.Datetime.now()
|
|
321
|
+
response.create_activity_on_exception()
|
|
322
|
+
create_response_activity = self._create_response_lines(
|
|
323
|
+
response=response, header=header, verifactu_response=res
|
|
324
|
+
)
|
|
325
|
+
updated_response_name = _("VERI*FACTU sending")
|
|
326
|
+
if create_exception:
|
|
327
|
+
updated_response_name = _("Connection error with VERI*FACTU")
|
|
328
|
+
elif create_response_activity:
|
|
329
|
+
updated_response_name = _("Incorrect invoices sent to VERI*FACTU")
|
|
330
|
+
response.name = updated_response_name
|
|
331
|
+
if create_response_activity:
|
|
332
|
+
response.create_send_response_activity()
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
def _create_response_lines(
|
|
336
|
+
self, response=False, header=False, verifactu_response=False
|
|
337
|
+
):
|
|
338
|
+
create_response_activity = False
|
|
339
|
+
# the returned object doesn't have `get` method, so use this form
|
|
340
|
+
verifactu_response_lines = (
|
|
341
|
+
"RespuestaLinea" in verifactu_response
|
|
342
|
+
and verifactu_response["RespuestaLinea"]
|
|
343
|
+
or []
|
|
344
|
+
)
|
|
345
|
+
models = self.env["verifactu.mixin"]._get_verifactu_reference_models()
|
|
346
|
+
for verifactu_response_line in verifactu_response_lines:
|
|
347
|
+
invoice_num = verifactu_response_line["IDFactura"]["NumSerieFactura"]
|
|
348
|
+
document = False
|
|
349
|
+
for model in models:
|
|
350
|
+
document = self.env[model].search(
|
|
351
|
+
[
|
|
352
|
+
("name", "=", invoice_num),
|
|
353
|
+
("id", "in", self.mapped("document_id")),
|
|
354
|
+
],
|
|
355
|
+
limit=1,
|
|
356
|
+
)
|
|
357
|
+
if document:
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
# Skip if document not found
|
|
361
|
+
if not document:
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Find the verifactu.invoice entry for this document
|
|
365
|
+
verifactu_invoice_entry = document.last_verifactu_invoice_entry_id
|
|
366
|
+
|
|
367
|
+
# Skip if no verifactu invoice entry found - this should not happen
|
|
368
|
+
# in normal flow but can happen in tests with mocked responses
|
|
369
|
+
if not verifactu_invoice_entry:
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
previous_response_line = document.last_verifactu_response_line_id
|
|
373
|
+
send_state = VERIFACTU_STATE_MAPPING[
|
|
374
|
+
verifactu_response_line["EstadoRegistro"]
|
|
375
|
+
]
|
|
376
|
+
vals = {
|
|
377
|
+
"entry_id": verifactu_invoice_entry.id,
|
|
378
|
+
"model": verifactu_invoice_entry.model,
|
|
379
|
+
"document_id": verifactu_invoice_entry.document_id,
|
|
380
|
+
"response": verifactu_response_line,
|
|
381
|
+
"entry_response_id": response.id,
|
|
382
|
+
"send_state": send_state,
|
|
383
|
+
"error_code": "CodigoErrorRegistro" in verifactu_response_line
|
|
384
|
+
and str(verifactu_response_line["CodigoErrorRegistro"])
|
|
385
|
+
or "",
|
|
386
|
+
}
|
|
387
|
+
response_line = (
|
|
388
|
+
self.env["verifactu.invoice.entry.response.line"].sudo().create(vals)
|
|
389
|
+
)
|
|
390
|
+
document.last_verifactu_response_line_id = response_line
|
|
391
|
+
verifactu_invoice_entry.last_response_line_id = response_line
|
|
392
|
+
self._process_response_line_doc_vals(
|
|
393
|
+
verifactu_response=verifactu_response,
|
|
394
|
+
verifactu_response_line=verifactu_response_line,
|
|
395
|
+
response_line=response_line,
|
|
396
|
+
previous_response_line=previous_response_line,
|
|
397
|
+
header_sent=header,
|
|
398
|
+
)
|
|
399
|
+
if send_state != "correct":
|
|
400
|
+
create_response_activity = True
|
|
401
|
+
return create_response_activity
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright 2025 ForgeFlow S.L.
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
3
|
+
from odoo import _, fields, models
|
|
4
|
+
|
|
5
|
+
VERIFACTU_STATE_MAPPING = {
|
|
6
|
+
"Correcto": "correct",
|
|
7
|
+
"Incorrecto": "incorrect",
|
|
8
|
+
"AceptadoConErrores": "accepted_with_errors",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VerifactuInvoiceEntryResponse(models.Model):
|
|
13
|
+
_name = "verifactu.invoice.entry.response"
|
|
14
|
+
_description = "VERI*FACTU Send Response"
|
|
15
|
+
_inherit = ["mail.activity.mixin", "mail.thread"]
|
|
16
|
+
_order = "id desc"
|
|
17
|
+
|
|
18
|
+
header = fields.Text()
|
|
19
|
+
name = fields.Char()
|
|
20
|
+
invoice_data = fields.Text()
|
|
21
|
+
response = fields.Text()
|
|
22
|
+
verifactu_csv = fields.Text(string="VERI*FACTU CSV")
|
|
23
|
+
date_response = fields.Datetime(readonly=True)
|
|
24
|
+
activity_type_id = fields.Many2one(
|
|
25
|
+
"mail.activity.type",
|
|
26
|
+
string="Activity Type",
|
|
27
|
+
compute="_compute_activity_type_id",
|
|
28
|
+
store=True,
|
|
29
|
+
)
|
|
30
|
+
response_line_ids = fields.One2many(
|
|
31
|
+
"verifactu.invoice.entry.response.line",
|
|
32
|
+
"entry_response_id",
|
|
33
|
+
string="Response lines",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def _compute_activity_type_id(self):
|
|
37
|
+
for record in self:
|
|
38
|
+
activity = self.env["mail.activity"].search(
|
|
39
|
+
[
|
|
40
|
+
("res_model", "=", "verifactu.invoice.entry.response"),
|
|
41
|
+
("res_id", "=", record.id),
|
|
42
|
+
],
|
|
43
|
+
limit=1,
|
|
44
|
+
)
|
|
45
|
+
record.activity_type_id = activity.activity_type_id if activity else False
|
|
46
|
+
|
|
47
|
+
def create_activity_on_exception(self):
|
|
48
|
+
model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
|
|
49
|
+
exception_activity_type = self.env.ref(
|
|
50
|
+
"l10n_es_verifactu_oca.mail_activity_data_exception"
|
|
51
|
+
)
|
|
52
|
+
activity_vals = []
|
|
53
|
+
responsible_group = self.env.ref(
|
|
54
|
+
"l10n_es_verifactu_oca.group_verifactu_responsible"
|
|
55
|
+
)
|
|
56
|
+
users = responsible_group.users
|
|
57
|
+
for record in self:
|
|
58
|
+
existing = self.env["mail.activity"].search_count(
|
|
59
|
+
[
|
|
60
|
+
("activity_type_id", "=", exception_activity_type.id),
|
|
61
|
+
("res_model", "=", "verifactu.invoice.entry.response"),
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
if not existing:
|
|
65
|
+
user = users[:1] or self.env.user
|
|
66
|
+
activity_vals.append(
|
|
67
|
+
{
|
|
68
|
+
"res_model_id": model_id,
|
|
69
|
+
"res_model": "verifactu.invoice.entry.response",
|
|
70
|
+
"res_id": record.id,
|
|
71
|
+
"activity_type_id": exception_activity_type.id,
|
|
72
|
+
"user_id": user.id,
|
|
73
|
+
"summary": _("Check connection error with VERI*FACTU"),
|
|
74
|
+
"note": _(
|
|
75
|
+
"There has been an error when trying to connect to "
|
|
76
|
+
"VERI*FACTU"
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
if activity_vals:
|
|
81
|
+
return self.env["mail.activity"].create(activity_vals)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def create_send_response_activity(self):
|
|
85
|
+
activity_type = self.env.ref("mail.mail_activity_data_warning")
|
|
86
|
+
model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
|
|
87
|
+
activity_vals = []
|
|
88
|
+
responsible_group = self.env.ref(
|
|
89
|
+
"l10n_es_verifactu_oca.group_verifactu_responsible"
|
|
90
|
+
)
|
|
91
|
+
users = responsible_group.users
|
|
92
|
+
for record in self:
|
|
93
|
+
user = users[:1] or self.env.user
|
|
94
|
+
activity_vals.append(
|
|
95
|
+
{
|
|
96
|
+
"activity_type_id": activity_type.id,
|
|
97
|
+
"user_id": user.id,
|
|
98
|
+
"res_id": record.id,
|
|
99
|
+
"res_model": "verifactu.invoice.entry.response",
|
|
100
|
+
"res_model_id": model_id,
|
|
101
|
+
"summary": _("Check incorrect invoices from VERI*FACTU"),
|
|
102
|
+
"note": _("There is an error with one or more invoices"),
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
return self.env["mail.activity"].create(activity_vals)
|
|
106
|
+
|
|
107
|
+
def complete_open_activity_on_exception(self):
|
|
108
|
+
exception_activity_type = self.env.ref(
|
|
109
|
+
"l10n_es_verifactu_oca.mail_activity_data_exception"
|
|
110
|
+
)
|
|
111
|
+
for _record in self:
|
|
112
|
+
activity = self.env["mail.activity"].search(
|
|
113
|
+
[
|
|
114
|
+
("activity_type_id", "=", exception_activity_type.id),
|
|
115
|
+
("res_model", "=", "verifactu.invoice.entry.response"),
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
for act in activity:
|
|
119
|
+
if act.state != "done":
|
|
120
|
+
act.action_done()
|
|
121
|
+
return True
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2025 ForgeFlow S.L.
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
|
|
6
|
+
from ..models.verifactu_invoice_entry import VERIFACTU_SEND_STATES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VerifactuInvoiceEntryResponseLine(models.Model):
|
|
10
|
+
_name = "verifactu.invoice.entry.response.line"
|
|
11
|
+
_description = "VERI*FACTU send log"
|
|
12
|
+
_order = "id desc"
|
|
13
|
+
|
|
14
|
+
entry_id = fields.Many2one(comodel_name="verifactu.invoice.entry", required=True)
|
|
15
|
+
entry_response_id = fields.Many2one("verifactu.invoice.entry.response")
|
|
16
|
+
model = fields.Char(readonly=True)
|
|
17
|
+
document_id = fields.Many2oneReference(
|
|
18
|
+
string="Document", model_field="model", readonly=True, index=True
|
|
19
|
+
)
|
|
20
|
+
response = fields.Text()
|
|
21
|
+
send_state = fields.Selection(
|
|
22
|
+
selection=VERIFACTU_SEND_STATES,
|
|
23
|
+
string="VERI*FACTU send state",
|
|
24
|
+
default="not_sent",
|
|
25
|
+
readonly=True,
|
|
26
|
+
copy=False,
|
|
27
|
+
help="Indicates the state of this document in relation with the "
|
|
28
|
+
"presentation to VERI*FACTU.",
|
|
29
|
+
)
|
|
30
|
+
verifactu_csv = fields.Text(related="entry_response_id.verifactu_csv")
|
|
31
|
+
error_code = fields.Char()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def document(self):
|
|
35
|
+
return self.env[self.model].browse(self.document_id).exists()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright 2024 Aures TIC - Almudena de La Puente <almudena@aurestic.es>
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
3
|
+
from odoo import _, api, exceptions, fields, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AeatVerifactuMap(models.Model):
|
|
7
|
+
_name = "verifactu.map"
|
|
8
|
+
_description = "VERI*FACTU mapping"
|
|
9
|
+
|
|
10
|
+
name = fields.Char(string="Model", required=True)
|
|
11
|
+
date_from = fields.Date()
|
|
12
|
+
date_to = fields.Date()
|
|
13
|
+
map_lines = fields.One2many(
|
|
14
|
+
comodel_name="verifactu.map.line",
|
|
15
|
+
inverse_name="verifactu_map_id",
|
|
16
|
+
string="Lines",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@api.constrains("date_from", "date_to")
|
|
20
|
+
def _unique_date_range(self):
|
|
21
|
+
for record in self:
|
|
22
|
+
record._unique_date_range_one()
|
|
23
|
+
|
|
24
|
+
def _unique_date_range_one(self):
|
|
25
|
+
# Based in l10n_es_aeat module
|
|
26
|
+
domain = [("id", "!=", self.id)]
|
|
27
|
+
if self.date_from and self.date_to:
|
|
28
|
+
domain += [
|
|
29
|
+
"|",
|
|
30
|
+
"&",
|
|
31
|
+
("date_from", "<=", self.date_to),
|
|
32
|
+
("date_from", ">=", self.date_from),
|
|
33
|
+
"|",
|
|
34
|
+
"&",
|
|
35
|
+
("date_to", "<=", self.date_to),
|
|
36
|
+
("date_to", ">=", self.date_from),
|
|
37
|
+
"|",
|
|
38
|
+
"&",
|
|
39
|
+
("date_from", "=", False),
|
|
40
|
+
("date_to", ">=", self.date_from),
|
|
41
|
+
"|",
|
|
42
|
+
"&",
|
|
43
|
+
("date_to", "=", False),
|
|
44
|
+
("date_from", "<=", self.date_to),
|
|
45
|
+
]
|
|
46
|
+
elif self.date_from:
|
|
47
|
+
domain += [("date_to", ">=", self.date_from)]
|
|
48
|
+
elif self.date_to:
|
|
49
|
+
domain += [("date_from", "<=", self.date_to)]
|
|
50
|
+
date_lst = self.search(domain)
|
|
51
|
+
if date_lst:
|
|
52
|
+
raise exceptions.UserError(
|
|
53
|
+
_("Error! The dates of the record overlap with an existing " "record.")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AeatVerifactuMapLines(models.Model):
|
|
58
|
+
_name = "verifactu.map.line"
|
|
59
|
+
_description = "VERI*FACTU mapping line"
|
|
60
|
+
|
|
61
|
+
code = fields.Char(required=True)
|
|
62
|
+
name = fields.Char()
|
|
63
|
+
taxes = fields.Many2many(comodel_name="account.tax.template")
|
|
64
|
+
verifactu_map_id = fields.Many2one(
|
|
65
|
+
comodel_name="verifactu.map", string="Parent mapping", ondelete="cascade"
|
|
66
|
+
)
|