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.

Files changed (74) hide show
  1. odoo/addons/l10n_es_verifactu_oca/README.rst +154 -0
  2. odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
  3. odoo/addons/l10n_es_verifactu_oca/__manifest__.py +48 -0
  4. odoo/addons/l10n_es_verifactu_oca/data/account_fiscal_position_template_data.xml +129 -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/verifactu_map_data.xml +120 -0
  9. odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +207 -0
  10. odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
  11. odoo/addons/l10n_es_verifactu_oca/hooks.py +43 -0
  12. odoo/addons/l10n_es_verifactu_oca/i18n/ca.po +1630 -0
  13. odoo/addons/l10n_es_verifactu_oca/i18n/ca_ES.po +1599 -0
  14. odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1640 -0
  15. odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1673 -0
  16. odoo/addons/l10n_es_verifactu_oca/models/__init__.py +16 -0
  17. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +40 -0
  18. odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position_template.py +18 -0
  19. odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
  20. odoo/addons/l10n_es_verifactu_oca/models/account_move.py +556 -0
  21. odoo/addons/l10n_es_verifactu_oca/models/aeat_mixin.py +163 -0
  22. odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
  23. odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
  24. odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +33 -0
  25. odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +30 -0
  26. odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
  27. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +401 -0
  28. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +121 -0
  29. odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +35 -0
  30. odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +66 -0
  31. odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +449 -0
  32. odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +24 -0
  33. odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.rst +18 -0
  34. odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.rst +19 -0
  35. odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.rst +1 -0
  36. odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.rst +4 -0
  37. odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.rst +15 -0
  38. odoo/addons/l10n_es_verifactu_oca/readme/USAGE.rst +1 -0
  39. odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +22 -0
  40. odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
  41. odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
  42. odoo/addons/l10n_es_verifactu_oca/static/description/index.html +508 -0
  43. odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +5 -0
  44. odoo/addons/l10n_es_verifactu_oca/tests/common.py +304 -0
  45. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_1.json +35 -0
  46. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_2.json +35 -0
  47. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
  48. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
  49. odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
  50. odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +451 -0
  51. odoo/addons/l10n_es_verifactu_oca/tests/test_account_journal.py +78 -0
  52. odoo/addons/l10n_es_verifactu_oca/tests/test_account_move_reversal.py +93 -0
  53. odoo/addons/l10n_es_verifactu_oca/tests/test_res_partner.py +48 -0
  54. odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +350 -0
  55. odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +30 -0
  56. odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +28 -0
  57. odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +219 -0
  58. odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
  59. odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +55 -0
  60. odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
  61. odoo/addons/l10n_es_verifactu_oca/views/res_partner_view.xml +27 -0
  62. odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +47 -0
  63. odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +48 -0
  64. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +149 -0
  65. odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +124 -0
  66. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +20 -0
  67. odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +53 -0
  68. odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +42 -0
  69. odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +1 -0
  70. odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
  71. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/METADATA +171 -0
  72. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/RECORD +74 -0
  73. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/WHEEL +5 -0
  74. odoo_addon_l10n_es_verifactu_oca-15.0.1.0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,556 @@
1
+ # Copyright 2024 Aures TIC - Almudena de La Puente
2
+ # Copyright 2024 Aures Tic - Jose Zambudio
3
+ # Copyright 2025 Tecnativa - Pedro M. Baeza
4
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
5
+
6
+ from collections import OrderedDict
7
+ from datetime import datetime
8
+
9
+ import pytz
10
+
11
+ from odoo import _, api, fields, models
12
+ from odoo.exceptions import UserError
13
+
14
+ VERIFACTU_VALID_INVOICE_STATES = ["posted"]
15
+
16
+
17
+ class AccountMove(models.Model):
18
+ _name = "account.move"
19
+ _inherit = ["account.move", "verifactu.mixin"]
20
+
21
+ verifactu_refund_specific_type = fields.Selection(
22
+ string="VERI*FACTU refund specific type",
23
+ selection=[
24
+ ("R1", "Art. 80.1 y 80.2 y error fundado en derecho"),
25
+ ("R2", "Art. 80.3"),
26
+ ("R3", "Art. 80.4"),
27
+ ("R4", "Resto"),
28
+ ("R5", "De factura simplificada"),
29
+ ],
30
+ help="Fill this field when the refund are one of the specific cases"
31
+ " of article 80 of LIVA for notifying to VERI*FACTU with the proper"
32
+ " invoice type.",
33
+ )
34
+
35
+ @api.depends("move_type")
36
+ def _compute_verifactu_refund_type(self):
37
+ refunds = self.filtered(lambda x: x.move_type == "out_refund")
38
+ refunds.verifactu_refund_type = "I"
39
+ (self - refunds).verifactu_refund_type = False
40
+
41
+ @api.depends("amount_total")
42
+ def _compute_verifactu_macrodata(self):
43
+ return super()._compute_verifactu_macrodata()
44
+
45
+ @api.depends(
46
+ "company_id",
47
+ "company_id.verifactu_enabled",
48
+ "company_id.verifactu_start_date",
49
+ "invoice_date",
50
+ "move_type",
51
+ "fiscal_position_id",
52
+ "fiscal_position_id.aeat_active",
53
+ "journal_id",
54
+ "journal_id.verifactu_enabled",
55
+ )
56
+ def _compute_verifactu_enabled(self):
57
+ """Compute if the invoice is enabled for the VERI*FACTU"""
58
+ for invoice in self:
59
+ if (
60
+ invoice.company_id.verifactu_enabled
61
+ and invoice.journal_id.verifactu_enabled
62
+ and invoice.move_type in ["out_invoice", "out_refund"]
63
+ ) and (
64
+ not invoice.company_id.verifactu_start_date
65
+ or invoice.invoice_date
66
+ and invoice.invoice_date >= invoice.company_id.verifactu_start_date
67
+ ):
68
+ invoice.verifactu_enabled = (
69
+ invoice.fiscal_position_id.aeat_active
70
+ if invoice.fiscal_position_id
71
+ else True
72
+ )
73
+ else:
74
+ invoice.verifactu_enabled = False
75
+
76
+ @api.depends("fiscal_position_id")
77
+ def _compute_verifactu_tax_key(self):
78
+ for document in self:
79
+ document.verifactu_tax_key = (
80
+ document.fiscal_position_id.verifactu_tax_key or "01"
81
+ )
82
+
83
+ @api.depends("fiscal_position_id")
84
+ def _compute_verifactu_registration_key(self):
85
+ for document in self:
86
+ if document.fiscal_position_id:
87
+ key = document.fiscal_position_id.verifactu_registration_key
88
+ if key:
89
+ document.verifactu_registration_key = key
90
+ else:
91
+ domain = [
92
+ ("code", "=", "01"),
93
+ ("verifactu_tax_key", "=", "01"),
94
+ ]
95
+ verifactu_key_obj = self.env["verifactu.registration.key"]
96
+ document.verifactu_registration_key = verifactu_key_obj.search(
97
+ domain, limit=1
98
+ )
99
+
100
+ def _get_verifactu_document_type(self):
101
+ invoice_type = ""
102
+ if self.move_type in ["out_invoice", "out_refund"]:
103
+ is_simplified = self._is_aeat_simplified_invoice()
104
+ invoice_type = "F2" if is_simplified else "F1"
105
+ if self.move_type == "out_refund":
106
+ if self.verifactu_refund_specific_type:
107
+ invoice_type = self.verifactu_refund_specific_type
108
+ else:
109
+ invoice_type = "R5" if is_simplified else "R1"
110
+ return invoice_type
111
+
112
+ def _get_verifactu_description(self):
113
+ return self.verifactu_description or self.company_id.verifactu_description
114
+
115
+ def _get_document_date(self):
116
+ """
117
+ TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that
118
+ it should be directly in l10n_es_aeat
119
+ """
120
+ return self.invoice_date
121
+
122
+ def _aeat_get_partner(self):
123
+ """
124
+ TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that
125
+ it should be directly in l10n_es_aeat
126
+ """
127
+ return self.commercial_partner_id
128
+
129
+ def _get_mapping_key(self):
130
+ """
131
+ TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that
132
+ it should be directly in l10n_es_aeat
133
+ """
134
+ return self.move_type
135
+
136
+ def _get_verifactu_valid_document_states(self):
137
+ return VERIFACTU_VALID_INVOICE_STATES
138
+
139
+ def _get_document_serial_number(self):
140
+ """
141
+ TODO: this method is the same in l10n_es_aeat_sii_oca, so I think that
142
+ it should be directly in l10n_es_aeat
143
+ """
144
+ serial_number = (self.name or "")[0:60]
145
+ if self.thirdparty_invoice:
146
+ serial_number = self.thirdparty_number[0:60]
147
+ return serial_number
148
+
149
+ def _get_verifactu_issuer(self):
150
+ return self.company_id.partner_id._parse_aeat_vat_info()[2]
151
+
152
+ def _get_verifactu_previous_hash(self):
153
+ if self.last_verifactu_invoice_entry_id:
154
+ return self.last_verifactu_invoice_entry_id.previous_hash or ""
155
+ return ""
156
+
157
+ def _get_verifactu_registration_date(self):
158
+ # Date format must be ISO 8601
159
+ return (
160
+ pytz.utc.localize(self.verifactu_registration_date)
161
+ .astimezone()
162
+ .isoformat(timespec="seconds")
163
+ )
164
+
165
+ def _get_verifactu_hash_string(self):
166
+ """Gets the VERI*FACTU hash string"""
167
+ if (
168
+ not self.verifactu_enabled
169
+ or self.state == "draft"
170
+ or self.move_type not in ("out_invoice", "out_refund")
171
+ ):
172
+ return ""
173
+ issuer = self._get_verifactu_issuer()
174
+ serial_number = self._get_document_serial_number()
175
+ expedition_date = self._get_verifactu_date(self._get_document_date())
176
+ document_type = self._get_verifactu_document_type()
177
+ _taxes_dict, amount_tax, amount_total = self._get_verifactu_taxes_and_total()
178
+ amount_tax = round(amount_tax, 2)
179
+ amount_total = round(amount_total, 2)
180
+ previous_hash = self._get_verifactu_previous_hash()
181
+ registration_date = self._get_verifactu_registration_date()
182
+ verifactu_hash_string = (
183
+ f"IDEmisorFactura={issuer}&"
184
+ f"NumSerieFactura={serial_number}&"
185
+ f"FechaExpedicionFactura={expedition_date}&"
186
+ f"TipoFactura={document_type}&"
187
+ f"CuotaTotal={amount_tax}&"
188
+ f"ImporteTotal={amount_total}&"
189
+ f"Huella={previous_hash}&"
190
+ f"FechaHoraHusoGenRegistro={registration_date}"
191
+ )
192
+ return verifactu_hash_string
193
+
194
+ def _get_verifactu_chaining(self):
195
+ return self.company_id.verifactu_chaining_id
196
+
197
+ def _get_verifactu_invoice_dict_out(self, cancel=False):
198
+ """Build dict with data to send to AEAT WS for document types:
199
+ out_invoice and out_refund.
200
+
201
+ :param cancel: It indicates if the dictionary is for sending a
202
+ cancellation of the document.
203
+ :return: documents (dict) : Dict XML with data for this document.
204
+ """
205
+ self.ensure_one()
206
+ document_date = self._get_verifactu_date(self._get_document_date())
207
+ company = self.company_id
208
+ serial_number = self._get_document_serial_number()
209
+ taxes_dict, amount_tax, amount_total = self._get_verifactu_taxes_and_total()
210
+ company_vat = company.partner_id._parse_aeat_vat_info()[2]
211
+ verifactu_doc_type = self._get_verifactu_document_type()
212
+ registroAlta = {}
213
+ inv_dict = {
214
+ "IDVersion": self._get_verifactu_version(),
215
+ "IDFactura": {
216
+ "IDEmisorFactura": company_vat,
217
+ "NumSerieFactura": serial_number,
218
+ "FechaExpedicionFactura": document_date,
219
+ },
220
+ "NombreRazonEmisor": self.company_id.name[0:120],
221
+ "TipoFactura": verifactu_doc_type,
222
+ }
223
+ if self.move_type == "out_refund":
224
+ inv_dict["TipoRectificativa"] = self.verifactu_refund_type
225
+ if self.verifactu_refund_type == "I":
226
+ inv_dict["FacturasRectificadas"] = []
227
+ origin = self.reversed_entry_id
228
+ if origin:
229
+ orig_document_date = self._get_verifactu_date(
230
+ origin._get_document_date()
231
+ )
232
+ orig_serial_number = origin._get_document_serial_number()
233
+ origin_data = {
234
+ "IDFacturaRectificada": {
235
+ "IDEmisorFactura": company_vat,
236
+ "NumSerieFactura": orig_serial_number,
237
+ "FechaExpedicionFactura": orig_document_date,
238
+ }
239
+ }
240
+ inv_dict["FacturasRectificadas"].append(origin_data)
241
+ # inv_dict["ImporteRectificacion"] = {
242
+ # "BaseRectificada": abs(origin.amount_untaxed_signed),
243
+ # "CuotaRectificada": abs(
244
+ # origin.amount_total_signed - origin.amount_untaxed_signed
245
+ # ),
246
+ # }
247
+ inv_dict["DescripcionOperacion"] = self._get_verifactu_description()
248
+ if verifactu_doc_type not in ("F2", "R5"):
249
+ inv_dict["Destinatarios"] = self._get_verifactu_receiver_dict()
250
+ elif verifactu_doc_type in ("F2", "R5"):
251
+ inv_dict["FacturaSinIdentifDestinatarioArt61d"] = "S"
252
+ inv_dict.update(
253
+ {
254
+ "Desglose": taxes_dict,
255
+ "CuotaTotal": amount_tax,
256
+ "ImporteTotal": amount_total,
257
+ "Encadenamiento": self._get_verifactu_chaining_invoice_dict(),
258
+ "SistemaInformatico": self._get_verifactu_developer_dict(),
259
+ "FechaHoraHusoGenRegistro": self._get_verifactu_registration_date(),
260
+ "TipoHuella": "01", # SHA-256
261
+ "Huella": self.verifactu_hash,
262
+ }
263
+ )
264
+ if self.aeat_state == "sent_w_errors":
265
+ # en caso de subsanación, debe generar un nuevo hash en la factura
266
+ inv_dict["Subsanacion"] = "S"
267
+ if self.last_verifactu_response_line_id.send_state == "incorrect":
268
+ inv_dict["RechazoPrevio"] = "S"
269
+ registroAlta.setdefault("RegistroAlta", inv_dict)
270
+ return registroAlta
271
+
272
+ def _get_verifactu_chaining_invoice_dict(self):
273
+ if self.last_verifactu_invoice_entry_id:
274
+ prev_entry = self.last_verifactu_invoice_entry_id.previous_invoice_entry_id
275
+ if prev_entry:
276
+ return {
277
+ "RegistroAnterior": {
278
+ "IDEmisorFactura": prev_entry.document._get_verifactu_issuer(),
279
+ "NumSerieFactura": prev_entry.document._get_document_serial_number(),
280
+ "FechaExpedicionFactura": prev_entry.document._get_verifactu_date(
281
+ prev_entry.document._get_document_date()
282
+ ),
283
+ "Huella": prev_entry.document_hash,
284
+ }
285
+ }
286
+ return {"PrimerRegistro": "S"}
287
+
288
+ def _get_verifactu_tax_dict(self, tax_line, tax_lines):
289
+ """Get the VERI*FACTU tax dictionary for the passed tax line.
290
+
291
+ :param self: Single invoice record.
292
+ :param tax_line: Tax line that is being analyzed.
293
+ :param tax_lines: Dictionary of processed invoice taxes for further operations
294
+ (like REQ).
295
+ :return: A dictionary with the corresponding VERI*FACTU tax values.
296
+ """
297
+ tax = tax_line["tax"]
298
+ tax_base_amount = tax_line["base"]
299
+ if tax.amount_type == "group":
300
+ tax_type = abs(tax.children_tax_ids.filtered("amount")[:1].amount)
301
+ else:
302
+ tax_type = abs(tax.amount)
303
+ tax_dict = {
304
+ "TipoImpositivo": str(tax_type),
305
+ "BaseImponibleOimporteNoSujeto": tax_base_amount,
306
+ }
307
+ key = "CuotaRepercutida"
308
+ tax_dict[key] = tax_line["amount"]
309
+ # Recargo de equivalencia
310
+ req_tax = self._get_verifactu_tax_req(tax)
311
+ if req_tax:
312
+ tax_dict["TipoRecargoEquivalencia"] = req_tax.amount
313
+ tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"]
314
+ return tax_dict
315
+
316
+ def _get_verifactu_tax_dict_ns(self, tax_line):
317
+ """Get the VERI*FACTU tax dictionary for the passed tax line.
318
+
319
+ :param self: Single invoice record.
320
+ :param tax_line: Tax line that is being analyzed.
321
+ :return: A dictionary with the corresponding VERI*FACTU tax values.
322
+ """
323
+ tax_base_amount = tax_line["base"]
324
+ tax_dict = {
325
+ "BaseImponibleOimporteNoSujeto": tax_base_amount,
326
+ }
327
+ return tax_dict
328
+
329
+ def _get_verifactu_tax_req(self, tax):
330
+ """Get the associated req tax for the specified tax.
331
+
332
+ :param self: Single invoice record.
333
+ :param tax: Initial tax for searching for the RE linked tax.
334
+ :return: REQ tax (or empty recordset) linked to the provided tax.
335
+ """
336
+ self.ensure_one()
337
+ document_date = self._get_document_date()
338
+ taxes_req = self._get_verifactu_taxes_map(["RE"], document_date)
339
+ re_lines = self.line_ids.filtered(
340
+ lambda x: tax in x.tax_ids and x.tax_ids & taxes_req
341
+ )
342
+ req_tax = re_lines.mapped("tax_ids") & taxes_req
343
+ if len(req_tax) > 1:
344
+ raise UserError(_("There's a mismatch in taxes for RE. Check them."))
345
+ return req_tax
346
+
347
+ def _get_verifactu_taxes_and_total(self):
348
+ self.ensure_one()
349
+ taxes_dict = {}
350
+ taxes_dict.setdefault("DetalleDesglose", [])
351
+ tax_lines = self._get_aeat_tax_info()
352
+ document_date = self._get_document_date()
353
+ taxes_S1 = self._get_verifactu_taxes_map(["S1"], document_date)
354
+ taxes_S2 = self._get_verifactu_taxes_map(["S2"], document_date)
355
+ taxes_N1 = self._get_verifactu_taxes_map(["N1"], document_date)
356
+ taxes_N2 = self._get_verifactu_taxes_map(["N2"], document_date)
357
+ taxes_RE = self._get_verifactu_taxes_map(["RE"], document_date)
358
+ taxes_not_in_total = self._get_verifactu_taxes_map(
359
+ ["TaxNotIncludedInTotal"], document_date
360
+ )
361
+ base_not_in_total = self._get_verifactu_taxes_map(
362
+ ["BaseNotIncludedInTotal"], document_date
363
+ )
364
+ excluded_taxes = taxes_not_in_total + base_not_in_total
365
+ breakdown_taxes = taxes_S1 + taxes_S2 + taxes_N1 + taxes_N2
366
+ not_in_amount_total = 0.0
367
+ not_in_taxes = 0.0
368
+ for tax_line in tax_lines.values():
369
+ tax = tax_line["tax"]
370
+ if tax in taxes_not_in_total:
371
+ not_in_amount_total += tax_line["amount"]
372
+ elif tax in base_not_in_total:
373
+ not_in_amount_total += tax_line["base"]
374
+ if tax in breakdown_taxes:
375
+ operation_type = self._get_verifactu_operation_type(
376
+ tax_line, taxes_S1, taxes_S2, taxes_N1, taxes_N2
377
+ )
378
+ tax_dict = {
379
+ "Impuesto": self.verifactu_tax_key,
380
+ "ClaveRegimen": self.verifactu_registration_key_code,
381
+ "CalificacionOperacion": operation_type,
382
+ }
383
+ if operation_type not in ("N1", "N2"):
384
+ new_tax_dict = self._get_verifactu_tax_dict(tax_line, tax_lines)
385
+ tax_dict.update(new_tax_dict)
386
+ else:
387
+ tax_dict.update(self._get_verifactu_tax_dict_ns(tax_line))
388
+ taxes_dict["DetalleDesglose"].append(tax_dict)
389
+ elif tax in excluded_taxes:
390
+ not_in_taxes += tax_line["amount"]
391
+ elif tax not in taxes_RE:
392
+ raise UserError(_("%s tax is not mapped to VERI*FACTU.", tax.name))
393
+ amount_tax = self.amount_tax_signed - not_in_taxes
394
+ amount_total = self.amount_total_signed - not_in_amount_total
395
+ return (
396
+ taxes_dict,
397
+ amount_tax,
398
+ amount_total,
399
+ )
400
+
401
+ def _get_verifactu_operation_type(
402
+ self, tax_line, taxes_S1, taxes_S2, taxes_N1, taxes_N2
403
+ ):
404
+ """
405
+ S1 Operación Sujeta y No exenta - Sin inversión del sujeto pasivo.
406
+ S2 Operación Sujeta y No exenta - Con Inversión del sujeto pasivo
407
+ N1 Operación No Sujeta artículo 7, 14, otros.
408
+ N2 Operación No Sujeta por Reglas de localización.
409
+ """
410
+ tax = tax_line["tax"]
411
+ if tax in taxes_S1:
412
+ return "S1"
413
+ elif tax in taxes_S2:
414
+ return "S2"
415
+ elif tax in taxes_N1:
416
+ return "N1"
417
+ elif tax in taxes_N2:
418
+ return "N2"
419
+ return "S1"
420
+
421
+ def _get_verifactu_receiver_dict(self):
422
+ self.ensure_one()
423
+ receiver = self._aeat_get_partner()
424
+ country_code, identifier_type, identifier = receiver._parse_aeat_vat_info()
425
+ if identifier:
426
+ identifier = "".join(e for e in identifier if e.isalnum()).upper()
427
+ else:
428
+ identifier = "NO_DISPONIBLE"
429
+ identifier_type = "06"
430
+ if identifier_type == "":
431
+ return {"IDDestinatario": {"NombreRazon": receiver.name, "NIF": identifier}}
432
+ if (
433
+ receiver._map_aeat_country_code(country_code)
434
+ in receiver._get_aeat_europe_codes()
435
+ ):
436
+ identifier = country_code + identifier
437
+ return {
438
+ "IDDestinatario": {
439
+ "NombreRazon": receiver.name,
440
+ "IDOtro": {
441
+ "CodigoPais": receiver.country_id.code,
442
+ "IDType": identifier_type,
443
+ "ID": identifier,
444
+ },
445
+ }
446
+ }
447
+
448
+ def _get_verifactu_qr_values(self):
449
+ """Get the QR values for the VERI*FACTU"""
450
+ self.ensure_one()
451
+ company_vat = self.company_id.partner_id._parse_aeat_vat_info()[2]
452
+ _taxes_dict, _amount_tax, amount_total = self._get_verifactu_taxes_and_total()
453
+ return OrderedDict(
454
+ [
455
+ ("nif", company_vat),
456
+ ("numserie", self.name),
457
+ ("fecha", self.invoice_date.strftime("%d-%m-%Y")),
458
+ ("importe", f"{amount_total:.2f}"), # noqa
459
+ ]
460
+ )
461
+
462
+ def _post(self, soft=True):
463
+ res = super()._post(soft=soft)
464
+ for record in self.sorted(lambda inv: inv.name):
465
+ if record.verifactu_enabled and record.aeat_state == "not_sent":
466
+ record._check_verifactu_configuration()
467
+ record.verifactu_registration_date = datetime.now()
468
+ record._generate_verifactu_chaining()
469
+ return res
470
+
471
+ def _check_verifactu_configuration(self, suffixes=None):
472
+ if not suffixes:
473
+ suffixes = []
474
+ # Too restrictive limitation
475
+ # if not self.fiscal_position_id:
476
+ # suffixes.append(_("- It does not have a fiscal position."))
477
+ if not self.verifactu_tax_key:
478
+ suffixes.append(_("- It does not have a tax key."))
479
+ if not self.verifactu_registration_key:
480
+ suffixes.append(_("- It does not have a registration key."))
481
+ if not self._check_inconsistent_taxes():
482
+ suffixes.append(_("- There are some inconsistent taxes on lines."))
483
+ if not self._check_all_taxes_mapped():
484
+ suffixes.append(_("- It does not have all taxes mapped."))
485
+ return super()._check_verifactu_configuration(suffixes=suffixes)
486
+
487
+ def _check_inconsistent_taxes(self):
488
+ document_date = self._get_document_date()
489
+ taxes_S1 = self._get_verifactu_taxes_map(["S1"], document_date)
490
+ taxes_S2 = self._get_verifactu_taxes_map(["S2"], document_date)
491
+ taxes_RE = self._get_verifactu_taxes_map(["RE"], document_date)
492
+ for line in self.invoice_line_ids:
493
+ taxes_in_s1 = line.tax_ids.filtered(lambda x: x in taxes_S1)
494
+ if len(taxes_in_s1) > 1:
495
+ return False
496
+ taxes_in_s2 = line.tax_ids.filtered(lambda x: x in taxes_S2)
497
+ if len(taxes_in_s2) > 1:
498
+ return False
499
+ taxes_in_RE = line.tax_ids.filtered(lambda x: x in taxes_RE)
500
+ if len(taxes_in_RE) > 1:
501
+ return False
502
+ return True
503
+
504
+ def _check_all_taxes_mapped(self):
505
+ tax_lines = self._get_aeat_tax_info()
506
+ if not tax_lines:
507
+ return False
508
+ verifactu_map = self._get_verifactu_map(self._get_document_date())
509
+ tax_templates = verifactu_map.map_lines.taxes
510
+ mapped_taxes = self.company_id.get_taxes_from_templates(tax_templates)
511
+ for tax_line in tax_lines.values():
512
+ if tax_line["tax"] not in mapped_taxes:
513
+ return False
514
+ return True
515
+
516
+ def cancel_verifactu(self):
517
+ raise NotImplementedError
518
+
519
+ def write(self, vals):
520
+ for invoice in self.filtered(
521
+ lambda x: x.is_invoice() and x.aeat_state != "not_sent"
522
+ ):
523
+ if invoice.move_type in ["out_invoice", "out_refund"]:
524
+ if "invoice_date" in vals:
525
+ self._raise_exception_verifactu(_("invoice date"))
526
+ elif "thirdparty_number" in vals:
527
+ self._raise_exception_verifactu(_("third-party number"))
528
+ elif "name" in vals:
529
+ self._raise_exception_verifactu(_("invoice number"))
530
+ return super().write(vals)
531
+
532
+ def button_cancel(self):
533
+ invoices_sent = self.filtered(
534
+ lambda inv: inv.verifactu_enabled and inv.aeat_state != "not_sent"
535
+ )
536
+ if invoices_sent:
537
+ raise UserError(_("You can not cancel invoices sent to VERI*FACTU."))
538
+ return super().button_cancel()
539
+
540
+ def button_draft(self):
541
+ invoices_sent = self.filtered(
542
+ lambda inv: inv.verifactu_enabled and inv.aeat_state != "not_sent"
543
+ )
544
+ if invoices_sent:
545
+ raise UserError(_("You can not set to draft invoices sent to VERI*FACTU."))
546
+ return super().button_draft()
547
+
548
+ def resend_verifactu(self):
549
+ for rec in self:
550
+ if (
551
+ rec.aeat_state == "sent_w_errors"
552
+ and rec.last_verifactu_invoice_entry_id
553
+ and not rec.last_verifactu_invoice_entry_id.send_state == "not_sent"
554
+ ):
555
+ rec.verifactu_registration_date = datetime.now()
556
+ rec._generate_verifactu_chaining(entry_type="modify")