odoo-addon-l10n-es-verifactu-oca 16.0.1.0.0.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of odoo-addon-l10n-es-verifactu-oca might be problematic. Click here for more details.

Files changed (68) hide show
  1. odoo/addons/l10n_es_verifactu_oca/README.rst +151 -0
  2. odoo/addons/l10n_es_verifactu_oca/__init__.py +2 -0
  3. odoo/addons/l10n_es_verifactu_oca/__manifest__.py +43 -0
  4. odoo/addons/l10n_es_verifactu_oca/data/account_fiscal_position_template_data.xml +149 -0
  5. odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
  6. odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +14 -0
  7. odoo/addons/l10n_es_verifactu_oca/data/mail_activity_data.xml +11 -0
  8. odoo/addons/l10n_es_verifactu_oca/data/neutralize.sql +2 -0
  9. odoo/addons/l10n_es_verifactu_oca/data/verifactu_map_data.xml +128 -0
  10. odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +207 -0
  11. odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
  12. odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1640 -0
  13. odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1598 -0
  14. odoo/addons/l10n_es_verifactu_oca/models/__init__.py +15 -0
  15. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +34 -0
  16. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position_template.py +18 -0
  17. odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
  18. odoo/addons/l10n_es_verifactu_oca/models/account_move.py +558 -0
  19. odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
  20. odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
  21. odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +26 -0
  22. odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +30 -0
  23. odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
  24. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +378 -0
  25. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +122 -0
  26. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +35 -0
  27. odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +66 -0
  28. odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +449 -0
  29. odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +24 -0
  30. odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.rst +18 -0
  31. odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.rst +16 -0
  32. odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.rst +1 -0
  33. odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.rst +4 -0
  34. odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.rst +15 -0
  35. odoo/addons/l10n_es_verifactu_oca/readme/USAGE.rst +1 -0
  36. odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +22 -0
  37. odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
  38. odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
  39. odoo/addons/l10n_es_verifactu_oca/static/description/index.html +505 -0
  40. odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +2 -0
  41. odoo/addons/l10n_es_verifactu_oca/tests/common.py +276 -0
  42. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_1.json +35 -0
  43. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_2.json +35 -0
  44. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
  45. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
  46. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
  47. odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +392 -0
  48. odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +348 -0
  49. odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +30 -0
  50. odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +28 -0
  51. odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +219 -0
  52. odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
  53. odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +26 -0
  54. odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
  55. odoo/addons/l10n_es_verifactu_oca/views/res_partner_view.xml +14 -0
  56. odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +47 -0
  57. odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +48 -0
  58. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +149 -0
  59. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +124 -0
  60. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +20 -0
  61. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +53 -0
  62. odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +42 -0
  63. odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +1 -0
  64. odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
  65. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/METADATA +168 -0
  66. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/RECORD +68 -0
  67. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/WHEEL +5 -0
  68. odoo_addon_l10n_es_verifactu_oca-16.0.1.0.0.22.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ # Copyright 2024 Aures Tic - Jose Zambudio <jose@aurestic.es>
2
+ # Copyright 2024 Aures TIC - Almudena de La Puente <almudena@aurestic.es>
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4
+
5
+ from odoo import fields, models
6
+
7
+
8
+ class AeatTaxAgency(models.Model):
9
+ _inherit = "aeat.tax.agency"
10
+
11
+ verifactu_wsdl_out = fields.Char(string="VERI*FACTU WSDL")
12
+ verifactu_wsdl_out_test_address = fields.Char(string="VERI*FACTU Test Address")
13
+ verifactu_qr_base_url = fields.Char(string="VERI*FACTU QR Base URL")
14
+ verifactu_qr_base_url_test_address = fields.Char(
15
+ string="VERI*FACTU QR Base URL Test"
16
+ )
17
+
18
+ def _connect_params_verifactu(self, company):
19
+ self.ensure_one()
20
+ wsdl_field = "verifactu_wsdl_out"
21
+ wsdl_test_field = wsdl_field + "_test_address"
22
+ port_name = "SistemaVerifactu"
23
+ address = self[wsdl_test_field] if company.verifactu_test else False
24
+ if not address and company.verifactu_test:
25
+ port_name += "Pruebas"
26
+ return {
27
+ "wsdl": self[wsdl_field],
28
+ "address": address,
29
+ "port_name": port_name,
30
+ }
@@ -0,0 +1,48 @@
1
+ # Copyright 2024 Aures TIC - Jose Zambudio
2
+ # Copyright 2025 Tecnativa - Pedro M. Baeza
3
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4
+
5
+ from odoo import fields, models
6
+
7
+
8
+ class ResCompany(models.Model):
9
+ _inherit = "res.company"
10
+
11
+ verifactu_enabled = fields.Boolean(string="VERI*FACTU enabled", tracking=True)
12
+ verifactu_test = fields.Boolean(
13
+ string="VERI*FACTU test environment?", tracking=True
14
+ )
15
+ verifactu_description = fields.Text(
16
+ string="VERI*FACTU description",
17
+ default="/",
18
+ help="The description for VERI*FACTU invoices if not set",
19
+ tracking=True,
20
+ )
21
+ verifactu_developer_id = fields.Many2one(
22
+ comodel_name="verifactu.developer",
23
+ string="VERI*FACTU developer",
24
+ ondelete="restrict",
25
+ tracking=True,
26
+ )
27
+ verifactu_start_date = fields.Date(
28
+ string="VERI*FACTU start date",
29
+ help="If this field is set, the VERI*FACTU won't be enabled on invoices with "
30
+ "lower invoice date. If not set, it can be enabled on all invoice dates",
31
+ tracking=True,
32
+ )
33
+ verifactu_chaining_id = fields.Many2one(
34
+ comodel_name="verifactu.chaining",
35
+ string="VERI*FACTU chaining",
36
+ ondelete="restrict",
37
+ tracking=True,
38
+ )
39
+
40
+ def write(self, vals):
41
+ # As the compute is not triggered automatically, we need to manually trigger it
42
+ # rewriting the flag at journal level.
43
+ res = super().write(vals)
44
+ if vals.get("verifactu_enabled"):
45
+ self.env["account.journal"].search(
46
+ [("company_id", "in", self.ids), ("type", "=", "sale")]
47
+ ).verifactu_enabled = True
48
+ return res
@@ -0,0 +1,26 @@
1
+ # Copyright 2024 Aures TIC - Jose Zambudio
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+
4
+ from odoo import api, fields, models
5
+
6
+
7
+ class ResPartner(models.Model):
8
+ _inherit = "res.partner"
9
+
10
+ verifactu_enabled = fields.Boolean(
11
+ compute="_compute_aeat_sending_enabled", string="VERI*FACTU enabled"
12
+ )
13
+
14
+ @api.depends("company_id")
15
+ def _compute_aeat_sending_enabled(self):
16
+ res = super()._compute_aeat_sending_enabled()
17
+ verifactu_enabled = any(self.env.companies.mapped("verifactu_enabled"))
18
+ for partner in self:
19
+ partner.verifactu_enabled = (
20
+ partner.company_id.verifactu_enabled
21
+ if partner.company_id
22
+ else verifactu_enabled
23
+ )
24
+ if partner.verifactu_enabled:
25
+ partner.aeat_sending_enabled = True
26
+ return res
@@ -0,0 +1,30 @@
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).
3
+
4
+ from odoo import fields, models
5
+
6
+
7
+ class VerifactuChaining(models.Model):
8
+ _name = "verifactu.chaining"
9
+ _inherit = "mail.thread"
10
+ _description = "VERI*FACTU chaining"
11
+
12
+ name = fields.Char(required=True, tracking=True)
13
+ last_verifactu_invoice_entry_id = fields.Many2one(
14
+ comodel_name="verifactu.invoice.entry",
15
+ string="Last invoice entry",
16
+ help="Reference to the last VERI*FACTU invoice entry for this company. "
17
+ "Used for atomic chaining.",
18
+ copy=False,
19
+ readonly=True,
20
+ )
21
+ sif_id = fields.Char(string="SIF ID", required=True, tracking=True)
22
+ installation_number = fields.Integer(default=1, required=True, tracking=True)
23
+
24
+ _sql_constraints = [
25
+ (
26
+ "verifactu_chaining_name_uniq",
27
+ "unique(name)",
28
+ "A chaining with the same name already exists!",
29
+ )
30
+ ]
@@ -0,0 +1,16 @@
1
+ # Copyright 2024 Aures TIC - Almudena de La Puente
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3
+
4
+ from odoo import fields, models
5
+
6
+
7
+ class VerifactuDeveloper(models.Model):
8
+ _name = "verifactu.developer"
9
+ _description = "VERI*FACTU developer"
10
+ _inherit = "mail.thread"
11
+
12
+ name = fields.Char(string="Developer Name", required=True, tracking=True)
13
+ vat = fields.Char(string="Developer VAT", required=True, tracking=True)
14
+ sif_name = fields.Char("SIF Name", required=True, tracking=True)
15
+ version = fields.Char(default="1.0", required=True, tracking=True)
16
+ responsibility_declaration = fields.Binary(attachment=True, copy=False)
@@ -0,0 +1,378 @@
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
+
7
+ from requests import Session
8
+ from zeep import Client, Settings
9
+ from zeep.plugins import HistoryPlugin
10
+ from zeep.transports import Transport
11
+
12
+ from odoo import _, api, fields, models
13
+ from odoo.exceptions import UserError
14
+ from odoo.tools import split_every
15
+
16
+ VERIFACTU_SEND_STATES = [
17
+ ("not_sent", "Not sent"),
18
+ ("correct", "Sent and Correct"),
19
+ ("incorrect", "Sent and Incorrect"),
20
+ ("accepted_with_errors", "Sent and accepted with errors"),
21
+ ]
22
+
23
+ VERIFACTU_STATE_MAPPING = {
24
+ "Correcto": "correct",
25
+ "Incorrecto": "incorrect",
26
+ "AceptadoConErrores": "accepted_with_errors",
27
+ }
28
+
29
+
30
+ class VerifactuInvoiceEntry(models.Model):
31
+ _name = "verifactu.invoice.entry"
32
+ _description = "VERI*FACTU invoice entry"
33
+ _order = "id desc"
34
+ _rec_name = "document_hash"
35
+
36
+ verifactu_chaining_id = fields.Many2one(
37
+ "verifactu.chaining", string="Chaining", ondelete="restrict", required=True
38
+ )
39
+ model = fields.Char(readonly=True, required=True)
40
+ document_id = fields.Many2oneReference(
41
+ string="Document", model_field="model", readonly=True, index=True, required=True
42
+ )
43
+ document_name = fields.Char(readonly=True)
44
+ previous_invoice_entry_id = fields.Many2one(
45
+ "verifactu.invoice.entry", string="Previous Invoice Entry", readonly=True
46
+ )
47
+ company_id = fields.Many2one(
48
+ "res.company", string="Company", required=True, readonly=True
49
+ )
50
+ document_hash = fields.Char(required=True, readonly=True)
51
+ aeat_json_data = fields.Text(
52
+ string="AEAT JSON Data",
53
+ help="Generated JSON data to send to AEAT",
54
+ readonly=True,
55
+ )
56
+ send_state = fields.Selection(
57
+ selection=VERIFACTU_SEND_STATES,
58
+ compute="_compute_send_state",
59
+ default="not_sent",
60
+ readonly=True,
61
+ store=True,
62
+ copy=False,
63
+ help="Indicates the state of this document in relation with the "
64
+ "presentation to VERI*FACTU.",
65
+ )
66
+ send_attempt = fields.Integer(
67
+ default=0, help="Number of attempts to send this document."
68
+ )
69
+ response_line_ids = fields.One2many(
70
+ "verifactu.invoice.entry.response.line",
71
+ "entry_id",
72
+ string="Responses",
73
+ help="Responses from VERI*FACTU after sending the documents.",
74
+ )
75
+ last_error_code = fields.Char(compute="_compute_last_error_code", store=True)
76
+ previous_hash = fields.Char(
77
+ related="previous_invoice_entry_id.document_hash",
78
+ readonly=True,
79
+ string="Previous Hash",
80
+ )
81
+ entry_type = fields.Selection(
82
+ selection=[
83
+ ("register", "Register"),
84
+ ("modify", "Modify"),
85
+ ("cancel", "Cancel"),
86
+ ],
87
+ default="register",
88
+ required=True,
89
+ )
90
+ last_response_line_id = fields.Many2one(
91
+ "verifactu.invoice.entry.response.line",
92
+ string="Last Response Line",
93
+ readonly=True,
94
+ )
95
+
96
+ @api.depends("response_line_ids", "response_line_ids.send_state")
97
+ def _compute_send_state(self):
98
+ for rec in self:
99
+ rec.send_state = "not_sent"
100
+ last_response = rec.last_response_line_id
101
+ if last_response:
102
+ rec.send_state = last_response.send_state
103
+
104
+ @api.depends("response_line_ids", "response_line_ids.error_code")
105
+ def _compute_last_error_code(self):
106
+ """Compute the last error code from the response lines."""
107
+ for rec in self:
108
+ if rec.last_response_line_id:
109
+ rec.last_error_code = rec.last_response_line_id.error_code
110
+ else:
111
+ rec.last_error_code = ""
112
+
113
+ @property
114
+ def document(self):
115
+ return self.env[self.model].browse(self.document_id).exists()
116
+
117
+ @api.model
118
+ def _cron_send_documents_to_verifactu(self):
119
+ batch_limit = self.env["verifactu.mixin"]._get_verifactu_batch()
120
+ for chaining in self.env["verifactu.chaining"].search([]):
121
+ self.env.cr.execute(
122
+ """
123
+ SELECT id FROM verifactu_invoice_entry AS vsq
124
+ WHERE vsq.send_state in ('not_sent', 'incorrect')
125
+ AND vsq.verifactu_chaining_id = %s
126
+ ORDER BY id
127
+ FOR UPDATE NOWAIT
128
+ """,
129
+ [chaining.id],
130
+ )
131
+ entries_to_send_ids = [entry[0] for entry in self.env.cr.fetchall()]
132
+ for entries_batch_ids in split_every(batch_limit, entries_to_send_ids):
133
+ records_to_send = self.browse(entries_batch_ids)
134
+ send_date = fields.Datetime.now()
135
+ threshold_time = send_date - datetime.timedelta(seconds=240)
136
+ # Look for documents where we have to send as an incident
137
+ outdated_records = records_to_send.filtered(
138
+ lambda r: r.document.verifactu_registration_date < threshold_time
139
+ )
140
+ current_records = records_to_send - outdated_records
141
+ outdated_records.with_context(
142
+ verifactu_incident=True
143
+ )._send_documents_to_verifactu()
144
+ current_records._send_documents_to_verifactu()
145
+ return True
146
+
147
+ def _get_verifactu_aeat_header(self):
148
+ """Builds VERI*FACTU send header
149
+
150
+ :param tipo_comunicacion String 'A0': new reg, 'A1': modification
151
+ :param cancellation Bool True when the communitacion es for document
152
+ cancellation
153
+ :return Dict with header data depending on cancellation
154
+ """
155
+ self.ensure_one()
156
+ if not self.company_id.vat:
157
+ raise UserError(
158
+ _("No VAT configured for the company '{}'").format(self.company_id.name)
159
+ )
160
+ header = {
161
+ "ObligadoEmision": {
162
+ "NombreRazon": self.company_id.name[0:120],
163
+ "NIF": self.company_id.partner_id._parse_aeat_vat_info()[2],
164
+ },
165
+ }
166
+ incident = self.env.context.get("verifactu_incident", False)
167
+ if incident:
168
+ header.update({"RemisionVoluntaria": {"Incidencia": "S"}})
169
+ return header
170
+
171
+ def _bind_verifactu_service(self, client, port_name, address=None):
172
+ self.ensure_one()
173
+ service = client._get_service("sfVerifactu")
174
+ port = client._get_port(service, port_name)
175
+ address = address or port.binding_options["address"]
176
+ return client.create_service(port.binding.name, address)
177
+
178
+ def _connect_verifactu_params_aeat(self):
179
+ self.ensure_one()
180
+ agency = self.company_id.tax_agency_id
181
+ if not agency:
182
+ # We use spanish agency by default to keep old behavior with
183
+ # ir.config parameters. In the future it might be good to reinforce
184
+ # to explicitly set a tax agency in the company by raising an error
185
+ # here.
186
+ agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
187
+ return agency._connect_params_verifactu(self.company_id)
188
+
189
+ def _connect_verifactu(self):
190
+ self.ensure_one()
191
+ public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates(
192
+ company=self.company_id
193
+ )
194
+ if not public_crt or not private_key:
195
+ raise UserError(
196
+ _("Please, configure the VERI*FACTU certificates for your company")
197
+ )
198
+ params = self._connect_verifactu_params_aeat()
199
+ session = Session()
200
+ session.cert = (public_crt, private_key)
201
+ transport = Transport(session=session)
202
+ history = HistoryPlugin()
203
+ settings = Settings(forbid_entities=False)
204
+ client = Client(
205
+ wsdl=params["wsdl"],
206
+ transport=transport,
207
+ plugins=[history],
208
+ settings=settings,
209
+ )
210
+ return self._bind_verifactu_service(
211
+ client, params["port_name"], params["address"]
212
+ )
213
+
214
+ def _process_response_line_doc_vals(
215
+ self,
216
+ verifactu_response=False,
217
+ verifactu_response_line=False,
218
+ response_line=False,
219
+ previous_response_line=False,
220
+ header_sent=False,
221
+ ):
222
+ estado_registro = verifactu_response_line["EstadoRegistro"]
223
+ doc_vals = {
224
+ "aeat_header_sent": json.dumps(header_sent, indent=4),
225
+ }
226
+ doc_vals["verifactu_return"] = verifactu_response_line
227
+ send_error = False
228
+ if hasattr(verifactu_response_line, "CodigoErrorRegistro"):
229
+ send_error = "{} | {}".format(
230
+ str(verifactu_response_line["CodigoErrorRegistro"]),
231
+ str(verifactu_response_line["DescripcionErrorRegistro"]),
232
+ )
233
+ # si ya ha devuelto previamente registro duplicado, parseamos el estado
234
+ # del registro duplicado para dejar la factura correcta o incorrecta
235
+ if (
236
+ verifactu_response_line["CodigoErrorRegistro"] == 3000
237
+ and previous_response_line
238
+ and (
239
+ previous_response_line.error_code == "3000"
240
+ and previous_response_line.send_state == "incorrect"
241
+ )
242
+ ):
243
+ registroDuplicado = verifactu_response_line["RegistroDuplicado"]
244
+ estado_registro = registroDuplicado["EstadoRegistroDuplicado"]
245
+ # en duplicados devuelve Correcta en vez de Correcto...
246
+ if estado_registro == "Correcta":
247
+ estado_registro = "Correcto"
248
+ response_line.send_state = "correct"
249
+ elif registroDuplicado["CodigoErrorRegistro"]:
250
+ # en duplicados devuelve AceptadaConErrores en vez de AceptadoConErrores...
251
+ if estado_registro == "AceptadaConErrores":
252
+ estado_registro = "AceptadoConErrores"
253
+ response_line.send_state = "accepted_with_errors"
254
+ send_error = "{} | {}".format(
255
+ str(registroDuplicado["CodigoErrorRegistro"]),
256
+ str(registroDuplicado["DescripcionErrorRegistro"]),
257
+ )
258
+ if estado_registro == "Correcto":
259
+ doc_vals.update(
260
+ {
261
+ "aeat_state": "sent",
262
+ "verifactu_csv": verifactu_response["CSV"],
263
+ "aeat_send_failed": False,
264
+ }
265
+ )
266
+ elif estado_registro == "AceptadoConErrores":
267
+ doc_vals.update(
268
+ {
269
+ "aeat_state": "sent_w_errors",
270
+ "verifactu_csv": verifactu_response["CSV"],
271
+ "aeat_send_failed": True,
272
+ }
273
+ )
274
+ else:
275
+ doc_vals["aeat_send_failed"] = True
276
+ doc_vals["aeat_send_error"] = send_error
277
+ if response_line.document_id:
278
+ response_line.document.write(doc_vals)
279
+ return doc_vals
280
+
281
+ def _send_documents_to_verifactu(self):
282
+ if not self:
283
+ return False
284
+ rec = self[0]
285
+ header = rec._get_verifactu_aeat_header()
286
+ registro_factura_list = []
287
+ create_exception = False
288
+ for rec in self:
289
+ rec.send_attempt += 1
290
+ if rec.document:
291
+ inv_dict = rec.document._get_verifactu_invoice_dict()
292
+ registro_factura_list.append(inv_dict)
293
+ try:
294
+ serv = rec._connect_verifactu()
295
+ res = serv.RegFactuSistemaFacturacion(header, registro_factura_list)
296
+ except Exception:
297
+ res = {}
298
+ create_exception = True
299
+ response_name = ""
300
+ response = (
301
+ self.env["verifactu.invoice.entry.response"]
302
+ .sudo()
303
+ .create(
304
+ {
305
+ "header": json.dumps(header),
306
+ "name": response_name,
307
+ "invoice_data": json.dumps(registro_factura_list),
308
+ "response": res,
309
+ "verifactu_csv": "CSV" in res and res["CSV"] or _("-"),
310
+ }
311
+ )
312
+ )
313
+ response.complete_open_activity_on_exception()
314
+ if create_exception:
315
+ if not response.date_response:
316
+ response.date_response = fields.Datetime.now()
317
+ response.create_activity_on_exception()
318
+ create_response_activity = self._create_response_lines(
319
+ response=response, header=header, verifactu_response=res
320
+ )
321
+ updated_response_name = _("VERI*FACTU sending")
322
+ if create_exception:
323
+ updated_response_name = _("Connection error with VERI*FACTU")
324
+ elif create_response_activity:
325
+ updated_response_name = _("Incorrect invoices sent to VERI*FACTU")
326
+ response.name = updated_response_name
327
+ if create_response_activity:
328
+ response.create_send_response_activity()
329
+ return True
330
+
331
+ def _create_response_lines(
332
+ self, response=False, header=False, verifactu_response=False
333
+ ):
334
+ create_response_activity = False
335
+ respuestaLineas = verifactu_response.get("RespuestaLinea", [])
336
+ for verifactu_response_line in respuestaLineas:
337
+ invoice_num = verifactu_response_line["IDFactura"]["NumSerieFactura"]
338
+ for model in self.env["verifactu.mixin"]._get_verifactu_reference_models():
339
+ if document := self.env[model].search(
340
+ [
341
+ ("name", "=", invoice_num),
342
+ ("id", "in", self.mapped("document_id")),
343
+ ],
344
+ limit=1,
345
+ ):
346
+ break
347
+ # Find the verifactu.invoice entry for this document
348
+ verifactu_invoice_entry = document.last_verifactu_invoice_entry_id
349
+ previous_response_line = document.last_verifactu_response_line_id
350
+ send_state = VERIFACTU_STATE_MAPPING[
351
+ verifactu_response_line["EstadoRegistro"]
352
+ ]
353
+ vals = {
354
+ "entry_id": verifactu_invoice_entry.id,
355
+ "model": verifactu_invoice_entry.model,
356
+ "document_id": verifactu_invoice_entry.document_id,
357
+ "response": verifactu_response_line,
358
+ "entry_response_id": response.id,
359
+ "send_state": send_state,
360
+ "error_code": "CodigoErrorRegistro" in verifactu_response_line
361
+ and str(verifactu_response_line["CodigoErrorRegistro"])
362
+ or "",
363
+ }
364
+ response_line = (
365
+ self.env["verifactu.invoice.entry.response.line"].sudo().create(vals)
366
+ )
367
+ document.last_verifactu_response_line_id = response_line
368
+ verifactu_invoice_entry.last_response_line_id = response_line
369
+ self._process_response_line_doc_vals(
370
+ verifactu_response=verifactu_response,
371
+ verifactu_response_line=verifactu_response_line,
372
+ response_line=response_line,
373
+ previous_response_line=previous_response_line,
374
+ header_sent=header,
375
+ )
376
+ if send_state != "correct":
377
+ create_response_activity = True
378
+ return create_response_activity
@@ -0,0 +1,122 @@
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
+ limit=1,
64
+ )
65
+ if not existing:
66
+ user = users[:1] or self.env.user
67
+ activity_vals.append(
68
+ {
69
+ "res_model_id": model_id,
70
+ "res_model": "verifactu.invoice.entry.response",
71
+ "res_id": record.id,
72
+ "activity_type_id": exception_activity_type.id,
73
+ "user_id": user.id,
74
+ "summary": _("Check connection error with VERI*FACTU"),
75
+ "note": _(
76
+ "There has been an error when trying to connect to "
77
+ "VERI*FACTU"
78
+ ),
79
+ }
80
+ )
81
+ if activity_vals:
82
+ return self.env["mail.activity"].create(activity_vals)
83
+ return False
84
+
85
+ def create_send_response_activity(self):
86
+ activity_type = self.env.ref("mail.mail_activity_data_warning")
87
+ model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
88
+ activity_vals = []
89
+ responsible_group = self.env.ref(
90
+ "l10n_es_verifactu_oca.group_verifactu_responsible"
91
+ )
92
+ users = responsible_group.users
93
+ for record in self:
94
+ user = users[:1] or self.env.user
95
+ activity_vals.append(
96
+ {
97
+ "activity_type_id": activity_type.id,
98
+ "user_id": user.id,
99
+ "res_id": record.id,
100
+ "res_model": "verifactu.invoice.entry.response",
101
+ "res_model_id": model_id,
102
+ "summary": _("Check incorrect invoices from VERI*FACTU"),
103
+ "note": _("There is an error with one or more invoices"),
104
+ }
105
+ )
106
+ return self.env["mail.activity"].create(activity_vals)
107
+
108
+ def complete_open_activity_on_exception(self):
109
+ exception_activity_type = self.env.ref(
110
+ "l10n_es_verifactu_oca.mail_activity_data_exception"
111
+ )
112
+ for _record in self:
113
+ activity = self.env["mail.activity"].search(
114
+ [
115
+ ("activity_type_id", "=", exception_activity_type.id),
116
+ ("res_model", "=", "verifactu.invoice.entry.response"),
117
+ ],
118
+ )
119
+ for act in activity:
120
+ if act.state != "done":
121
+ act.action_done()
122
+ return True