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

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