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,631 @@
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
+ # Don't use third party number for now, until
146
+ # the full third party invoice management is implemented.
147
+ # if self.thirdparty_invoice:
148
+ # serial_number = self.thirdparty_number[0:60]
149
+ return serial_number
150
+
151
+ def _get_verifactu_issuer(self):
152
+ return self.company_id.partner_id._parse_aeat_vat_info()[2]
153
+
154
+ def _get_verifactu_previous_hash(self):
155
+ if self.last_verifactu_invoice_entry_id:
156
+ return self.last_verifactu_invoice_entry_id.previous_hash or ""
157
+ return ""
158
+
159
+ def _get_verifactu_registration_date(self):
160
+ # Date format must be ISO 8601
161
+ return (
162
+ pytz.utc.localize(self.verifactu_registration_date)
163
+ .astimezone()
164
+ .isoformat(timespec="seconds")
165
+ )
166
+
167
+ def _get_verifactu_hash_string(self, cancel=False):
168
+ """Gets the VERI*FACTU hash string"""
169
+ if (
170
+ not self.verifactu_enabled
171
+ or self.state == "draft"
172
+ or self.move_type not in ("out_invoice", "out_refund")
173
+ ):
174
+ return ""
175
+ issuer = self._get_verifactu_issuer()
176
+ serial_number = self._get_document_serial_number()
177
+ expedition_date = self._get_verifactu_date(self._get_document_date())
178
+ document_type = self._get_verifactu_document_type()
179
+ _taxes_dict, amount_tax, amount_total = self._get_verifactu_taxes_and_total()
180
+ amount_tax = round(amount_tax, 2)
181
+ amount_total = round(amount_total, 2)
182
+ previous_hash = self._get_verifactu_previous_hash()
183
+ registration_date = self._get_verifactu_registration_date()
184
+ if not cancel:
185
+ verifactu_hash_string = (
186
+ f"IDEmisorFactura={issuer}&"
187
+ f"NumSerieFactura={serial_number}&"
188
+ f"FechaExpedicionFactura={expedition_date}&"
189
+ f"TipoFactura={document_type}&"
190
+ f"CuotaTotal={amount_tax}&"
191
+ f"ImporteTotal={amount_total}&"
192
+ f"Huella={previous_hash}&"
193
+ f"FechaHoraHusoGenRegistro={registration_date}"
194
+ )
195
+ else:
196
+ verifactu_hash_string = (
197
+ f"IDEmisorFacturaAnulada={issuer}&"
198
+ f"NumSerieFacturaAnulada={serial_number}&"
199
+ f"FechaExpedicionFacturaAnulada={expedition_date}&"
200
+ f"Huella={previous_hash}&"
201
+ f"FechaHoraHusoGenRegistro={registration_date}"
202
+ )
203
+ return verifactu_hash_string
204
+
205
+ def _get_verifactu_chaining(self):
206
+ return self.company_id.verifactu_chaining_id
207
+
208
+ def _get_verifactu_invoice_dict_out(self):
209
+ """Build dict with data to send to AEAT WS for document types:
210
+ out_invoice and out_refund.
211
+ :return: documents (dict) : Dict XML with data for this document.
212
+ """
213
+ self.ensure_one()
214
+ document_date = self._get_verifactu_date(self._get_document_date())
215
+ company = self.company_id
216
+ serial_number = self._get_document_serial_number()
217
+ taxes_dict, amount_tax, amount_total = self._get_verifactu_taxes_and_total()
218
+ company_vat = company.partner_id._parse_aeat_vat_info()[2]
219
+ verifactu_doc_type = self._get_verifactu_document_type()
220
+ registroAlta = {}
221
+ inv_dict = {
222
+ "IDVersion": self._get_verifactu_version(),
223
+ "IDFactura": {
224
+ "IDEmisorFactura": company_vat,
225
+ "NumSerieFactura": serial_number,
226
+ "FechaExpedicionFactura": document_date,
227
+ },
228
+ "NombreRazonEmisor": self.company_id.name[0:120],
229
+ "TipoFactura": verifactu_doc_type,
230
+ }
231
+ if self.move_type == "out_refund":
232
+ inv_dict["TipoRectificativa"] = self.verifactu_refund_type
233
+ if self.verifactu_refund_type == "I":
234
+ inv_dict["FacturasRectificadas"] = []
235
+ origin = self.reversed_entry_id
236
+ if origin:
237
+ orig_document_date = self._get_verifactu_date(
238
+ origin._get_document_date()
239
+ )
240
+ orig_serial_number = origin._get_document_serial_number()
241
+ origin_data = {
242
+ "IDFacturaRectificada": {
243
+ "IDEmisorFactura": company_vat,
244
+ "NumSerieFactura": orig_serial_number,
245
+ "FechaExpedicionFactura": orig_document_date,
246
+ }
247
+ }
248
+ inv_dict["FacturasRectificadas"].append(origin_data)
249
+ # inv_dict["ImporteRectificacion"] = {
250
+ # "BaseRectificada": abs(origin.amount_untaxed_signed),
251
+ # "CuotaRectificada": abs(
252
+ # origin.amount_total_signed - origin.amount_untaxed_signed
253
+ # ),
254
+ # }
255
+ inv_dict["DescripcionOperacion"] = self._get_verifactu_description()
256
+ if verifactu_doc_type not in ("F2", "R5"):
257
+ inv_dict["Destinatarios"] = self._get_verifactu_receiver_dict()
258
+ elif verifactu_doc_type in ("F2", "R5"):
259
+ inv_dict["FacturaSinIdentifDestinatarioArt61d"] = "S"
260
+ inv_dict.update(
261
+ {
262
+ "Desglose": taxes_dict,
263
+ "CuotaTotal": amount_tax,
264
+ "ImporteTotal": amount_total,
265
+ "Encadenamiento": self._get_verifactu_chaining_invoice_dict(),
266
+ "SistemaInformatico": self._get_verifactu_developer_dict(),
267
+ "FechaHoraHusoGenRegistro": self._get_verifactu_registration_date(),
268
+ "TipoHuella": "01", # SHA-256
269
+ "Huella": self.verifactu_hash,
270
+ }
271
+ )
272
+ if self.aeat_state in ("sent_w_errors", "incorrect"):
273
+ # en caso de subsanación, debe generar un nuevo hash en la factura
274
+ inv_dict["Subsanacion"] = "S"
275
+ if self.aeat_state == "incorrect":
276
+ inv_dict["RechazoPrevio"] = "X"
277
+ registroAlta.setdefault("RegistroAlta", inv_dict)
278
+ return registroAlta
279
+
280
+ def _get_verifactu_cancel_invoice_dict_out(self):
281
+ """Build cancel dict with data to send to AEAT WS for document types:
282
+ out_invoice and out_refund.
283
+ :return: documents (dict) : Dict XML with data for this document.
284
+ """
285
+ self.ensure_one()
286
+ document_date = self._get_verifactu_date(self._get_document_date())
287
+ company = self.company_id
288
+ serial_number = self._get_document_serial_number()
289
+ company_vat = company.partner_id._parse_aeat_vat_info()[2]
290
+ registroAnulacion = {}
291
+ inv_dict = {
292
+ "IDVersion": self._get_verifactu_version(),
293
+ "IDFactura": {
294
+ "IDEmisorFacturaAnulada": company_vat,
295
+ "NumSerieFacturaAnulada": serial_number,
296
+ "FechaExpedicionFacturaAnulada": document_date,
297
+ },
298
+ }
299
+ if self.aeat_state == "cancel_incorrect":
300
+ inv_dict["RechazoPrevio"] = "S"
301
+ inv_dict.update(
302
+ {
303
+ "Encadenamiento": self._get_verifactu_chaining_invoice_dict(),
304
+ "SistemaInformatico": self._get_verifactu_developer_dict(),
305
+ "FechaHoraHusoGenRegistro": self._get_verifactu_registration_date(),
306
+ "TipoHuella": "01", # SHA-256
307
+ "Huella": self.verifactu_hash,
308
+ }
309
+ )
310
+ registroAnulacion.setdefault("RegistroAnulacion", inv_dict)
311
+ return registroAnulacion
312
+
313
+ def _get_verifactu_chaining_invoice_dict(self):
314
+ if self.last_verifactu_invoice_entry_id:
315
+ prev_entry = self.last_verifactu_invoice_entry_id.previous_invoice_entry_id
316
+ if prev_entry:
317
+ doc = prev_entry.document
318
+ return {
319
+ "RegistroAnterior": {
320
+ "IDEmisorFactura": doc._get_verifactu_issuer(),
321
+ "NumSerieFactura": doc._get_document_serial_number(),
322
+ "FechaExpedicionFactura": doc._get_verifactu_date(
323
+ doc._get_document_date()
324
+ ),
325
+ "Huella": prev_entry.document_hash,
326
+ }
327
+ }
328
+ return {"PrimerRegistro": "S"}
329
+
330
+ def _get_verifactu_tax_dict(self, tax_line, tax_lines):
331
+ """Get the VERI*FACTU tax dictionary for the passed tax line.
332
+
333
+ :param self: Single invoice record.
334
+ :param tax_line: Tax line that is being analyzed.
335
+ :param tax_lines: Dictionary of processed invoice taxes for further operations
336
+ (like REQ).
337
+ :return: A dictionary with the corresponding VERI*FACTU tax values.
338
+ """
339
+ tax = tax_line["tax"]
340
+ tax_base_amount = tax_line["base"]
341
+ if tax.amount_type == "group":
342
+ tax_type = abs(tax.children_tax_ids.filtered("amount")[:1].amount)
343
+ else:
344
+ tax_type = abs(tax.amount)
345
+ tax_dict = {
346
+ "TipoImpositivo": str(tax_type),
347
+ "BaseImponibleOimporteNoSujeto": tax_base_amount,
348
+ }
349
+ key = "CuotaRepercutida"
350
+ tax_dict[key] = tax_line["amount"]
351
+ # Recargo de equivalencia
352
+ req_tax = self._get_verifactu_tax_req(tax)
353
+ if req_tax:
354
+ tax_dict["TipoRecargoEquivalencia"] = req_tax.amount
355
+ tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"]
356
+ return tax_dict
357
+
358
+ def _get_verifactu_tax_dict_ns(self, tax_line):
359
+ """Get the VERI*FACTU tax dictionary for the passed tax line.
360
+
361
+ :param self: Single invoice record.
362
+ :param tax_line: Tax line that is being analyzed.
363
+ :return: A dictionary with the corresponding VERI*FACTU tax values.
364
+ """
365
+ tax_base_amount = tax_line["base"]
366
+ tax_dict = {
367
+ "BaseImponibleOimporteNoSujeto": tax_base_amount,
368
+ }
369
+ return tax_dict
370
+
371
+ def _get_verifactu_tax_req(self, tax):
372
+ """Get the associated req tax for the specified tax.
373
+
374
+ :param self: Single invoice record.
375
+ :param tax: Initial tax for searching for the RE linked tax.
376
+ :return: REQ tax (or empty recordset) linked to the provided tax.
377
+ """
378
+ self.ensure_one()
379
+ document_date = self._get_document_date()
380
+ taxes_req = self._get_verifactu_taxes_map(["RE"], document_date)
381
+ re_lines = self.line_ids.filtered(
382
+ lambda x: tax in x.tax_ids and x.tax_ids & taxes_req
383
+ )
384
+ req_tax = re_lines.mapped("tax_ids") & taxes_req
385
+ if len(req_tax) > 1:
386
+ raise UserError(_("There's a mismatch in taxes for RE. Check them."))
387
+ return req_tax
388
+
389
+ def _get_verifactu_taxes_and_total(self):
390
+ self.ensure_one()
391
+ taxes_dict = {}
392
+ taxes_dict.setdefault("DetalleDesglose", [])
393
+ tax_lines = self._get_aeat_tax_info()
394
+ document_date = self._get_document_date()
395
+ taxes_S1 = self._get_verifactu_taxes_map(["S1"], document_date)
396
+ taxes_S2 = self._get_verifactu_taxes_map(["S2"], document_date)
397
+ taxes_N1 = self._get_verifactu_taxes_map(["N1"], document_date)
398
+ taxes_N2 = self._get_verifactu_taxes_map(["N2"], document_date)
399
+ taxes_RE = self._get_verifactu_taxes_map(["RE"], document_date)
400
+ taxes_not_in_total = self._get_verifactu_taxes_map(
401
+ ["TaxNotIncludedInTotal"], document_date
402
+ )
403
+ base_not_in_total = self._get_verifactu_taxes_map(
404
+ ["BaseNotIncludedInTotal"], document_date
405
+ )
406
+ excluded_taxes = taxes_not_in_total + base_not_in_total
407
+ breakdown_taxes = taxes_S1 + taxes_S2 + taxes_N1 + taxes_N2
408
+ not_in_amount_total = 0.0
409
+ not_in_taxes = 0.0
410
+ for tax_line in tax_lines.values():
411
+ tax = tax_line["tax"]
412
+ if tax in taxes_not_in_total:
413
+ not_in_amount_total += tax_line["amount"]
414
+ elif tax in base_not_in_total:
415
+ not_in_amount_total += tax_line["base"]
416
+ if tax in breakdown_taxes:
417
+ operation_type = self._get_verifactu_operation_type(
418
+ tax_line, taxes_S1, taxes_S2, taxes_N1, taxes_N2
419
+ )
420
+ tax_dict = {
421
+ "Impuesto": self.verifactu_tax_key,
422
+ "ClaveRegimen": self.verifactu_registration_key_code,
423
+ "CalificacionOperacion": operation_type,
424
+ }
425
+ if operation_type not in ("N1", "N2"):
426
+ new_tax_dict = self._get_verifactu_tax_dict(tax_line, tax_lines)
427
+ tax_dict.update(new_tax_dict)
428
+ else:
429
+ tax_dict.update(self._get_verifactu_tax_dict_ns(tax_line))
430
+ taxes_dict["DetalleDesglose"].append(tax_dict)
431
+ elif tax in excluded_taxes:
432
+ not_in_taxes += tax_line["amount"]
433
+ elif tax not in taxes_RE:
434
+ raise UserError(_("%s tax is not mapped to VERI*FACTU.", tax.name))
435
+ amount_tax = self.amount_tax_signed - not_in_taxes
436
+ amount_total = self.amount_total_signed - not_in_amount_total
437
+ return (
438
+ taxes_dict,
439
+ amount_tax,
440
+ amount_total,
441
+ )
442
+
443
+ def _get_verifactu_operation_type(
444
+ self, tax_line, taxes_S1, taxes_S2, taxes_N1, taxes_N2
445
+ ):
446
+ """
447
+ S1 Operación Sujeta y No exenta - Sin inversión del sujeto pasivo.
448
+ S2 Operación Sujeta y No exenta - Con Inversión del sujeto pasivo
449
+ N1 Operación No Sujeta artículo 7, 14, otros.
450
+ N2 Operación No Sujeta por Reglas de localización.
451
+ """
452
+ tax = tax_line["tax"]
453
+ if tax in taxes_S1:
454
+ return "S1"
455
+ elif tax in taxes_S2:
456
+ return "S2"
457
+ elif tax in taxes_N1:
458
+ return "N1"
459
+ elif tax in taxes_N2:
460
+ return "N2"
461
+ return "S1"
462
+
463
+ def _get_verifactu_receiver_dict(self):
464
+ self.ensure_one()
465
+ receiver = self._aeat_get_partner()
466
+ country_code, identifier_type, identifier = receiver._parse_aeat_vat_info()
467
+ if identifier:
468
+ identifier = "".join(e for e in identifier if e.isalnum()).upper()
469
+ else:
470
+ identifier = "NO_DISPONIBLE"
471
+ identifier_type = "06"
472
+ if identifier_type == "":
473
+ return {"IDDestinatario": {"NombreRazon": receiver.name, "NIF": identifier}}
474
+ if (
475
+ receiver._map_aeat_country_code(country_code)
476
+ in receiver._get_aeat_europe_codes()
477
+ ):
478
+ identifier = country_code + identifier
479
+ return {
480
+ "IDDestinatario": {
481
+ "NombreRazon": receiver.name,
482
+ "IDOtro": {
483
+ "CodigoPais": receiver.country_id.code,
484
+ "IDType": identifier_type,
485
+ "ID": identifier,
486
+ },
487
+ }
488
+ }
489
+
490
+ def _get_verifactu_qr_values(self):
491
+ """Get the QR values for the VERI*FACTU"""
492
+ self.ensure_one()
493
+ company_vat = self.company_id.partner_id._parse_aeat_vat_info()[2]
494
+ _taxes_dict, _amount_tax, amount_total = self._get_verifactu_taxes_and_total()
495
+ return OrderedDict(
496
+ [
497
+ ("nif", company_vat),
498
+ ("numserie", self.name),
499
+ ("fecha", self.invoice_date.strftime("%d-%m-%Y")),
500
+ ("importe", f"{amount_total:.2f}"), # noqa
501
+ ]
502
+ )
503
+
504
+ def _post(self, soft=True):
505
+ res = super()._post(soft=soft)
506
+ for record in self.sorted(lambda inv: inv.name):
507
+ if record.verifactu_enabled and record.aeat_state == "not_sent":
508
+ record._check_verifactu_configuration()
509
+ record.verifactu_registration_date = datetime.now()
510
+ record._generate_verifactu_chaining()
511
+ return res
512
+
513
+ def _check_verifactu_configuration(self, suffixes=None):
514
+ if not suffixes:
515
+ suffixes = []
516
+ # Too restrictive limitation
517
+ # if not self.fiscal_position_id:
518
+ # suffixes.append(_("- It does not have a fiscal position."))
519
+ if not self.verifactu_tax_key:
520
+ suffixes.append(_("- It does not have a tax key."))
521
+ if not self.verifactu_registration_key:
522
+ suffixes.append(_("- It does not have a registration key."))
523
+ if not self._check_inconsistent_taxes():
524
+ suffixes.append(_("- There are some inconsistent taxes on lines."))
525
+ if not self._check_all_taxes_mapped():
526
+ suffixes.append(_("- It does not have all taxes mapped."))
527
+ return super()._check_verifactu_configuration(suffixes=suffixes)
528
+
529
+ def _check_inconsistent_taxes(self):
530
+ document_date = self._get_document_date()
531
+ taxes_S1 = self._get_verifactu_taxes_map(["S1"], document_date)
532
+ taxes_S2 = self._get_verifactu_taxes_map(["S2"], document_date)
533
+ taxes_RE = self._get_verifactu_taxes_map(["RE"], document_date)
534
+ for line in self.invoice_line_ids:
535
+ taxes_in_s1 = line.tax_ids.filtered(lambda x: x in taxes_S1)
536
+ if len(taxes_in_s1) > 1:
537
+ return False
538
+ taxes_in_s2 = line.tax_ids.filtered(lambda x: x in taxes_S2)
539
+ if len(taxes_in_s2) > 1:
540
+ return False
541
+ taxes_in_RE = line.tax_ids.filtered(lambda x: x in taxes_RE)
542
+ if len(taxes_in_RE) > 1:
543
+ return False
544
+ return True
545
+
546
+ def _check_all_taxes_mapped(self):
547
+ if not (tax_lines := self._get_aeat_tax_info()):
548
+ return False
549
+ verifactu_map = self._get_verifactu_map(self._get_document_date())
550
+ tax_templates = verifactu_map.map_lines.tax_xmlid_ids
551
+ mapped_taxes = self.env["account.tax"]
552
+ for template in tax_templates:
553
+ tax_id = self.company_id._get_tax_id_from_xmlid(template.name)
554
+ mapped_taxes |= self.env["account.tax"].browse(tax_id)
555
+ for tax_line in tax_lines.values():
556
+ if tax_line["tax"] not in mapped_taxes:
557
+ return False
558
+ return True
559
+
560
+ def cancel_verifactu(self):
561
+ self.ensure_one()
562
+ if (
563
+ self.aeat_state
564
+ in (
565
+ "sent_w_errors",
566
+ "sent",
567
+ "cancel_incorrect",
568
+ "cancel_w_errors",
569
+ )
570
+ and self.last_verifactu_invoice_entry_id
571
+ and not self.last_verifactu_invoice_entry_id.send_state == "not_sent"
572
+ ):
573
+ if self.state != "cancel":
574
+ action = self.env["ir.actions.act_window"]._for_xml_id(
575
+ "l10n_es_verifactu_oca.verifactu_cancel_invoice_wizard_action"
576
+ )
577
+ action["context"] = {
578
+ "default_invoice_id": self.id,
579
+ }
580
+ return action
581
+ entry_type = "cancel"
582
+ self.verifactu_registration_date = datetime.now()
583
+ self._generate_verifactu_chaining(entry_type=entry_type)
584
+
585
+ def write(self, vals):
586
+ for invoice in self.filtered(
587
+ lambda x: x.is_invoice() and x.aeat_state != "not_sent"
588
+ ):
589
+ if invoice.move_type in ["out_invoice", "out_refund"]:
590
+ if "invoice_date" in vals:
591
+ self._raise_exception_verifactu(_("invoice date"))
592
+ elif "thirdparty_number" in vals:
593
+ self._raise_exception_verifactu(_("third-party number"))
594
+ elif "name" in vals:
595
+ self._raise_exception_verifactu(_("invoice number"))
596
+ return super().write(vals)
597
+
598
+ def button_cancel(self):
599
+ invoices_sent = self.filtered(
600
+ lambda inv: inv.verifactu_enabled and inv.aeat_state != "not_sent"
601
+ )
602
+ if invoices_sent and not self.env.context.get("verifactu_cancel"):
603
+ raise UserError(_("You can not cancel invoices sent to VERI*FACTU."))
604
+ return super().button_cancel()
605
+
606
+ def _check_draftable(self):
607
+ # Don't block the intermediate pass to draft when cancelling VERI*FACTU invoice
608
+ if not self.env.context.get("verifactu_cancel"):
609
+ return super()._check_draftable()
610
+
611
+ def button_draft(self):
612
+ # Don't allow go to draft, except when cancelling VERI*FACTU invoice via wizard
613
+ invoices_sent = self.filtered(
614
+ lambda inv: inv.verifactu_enabled and inv.aeat_state != "not_sent"
615
+ )
616
+ if invoices_sent and not self.env.context.get("verifactu_cancel"):
617
+ raise UserError(_("You can not set to draft invoices sent to VERI*FACTU."))
618
+ return super().button_draft()
619
+
620
+ def resend_verifactu(self):
621
+ for rec in self:
622
+ if (
623
+ rec.aeat_state in ("sent_w_errors", "incorrect")
624
+ and rec.last_verifactu_invoice_entry_id
625
+ and not rec.last_verifactu_invoice_entry_id.send_state == "not_sent"
626
+ ):
627
+ entry_type = (
628
+ "modify" if rec.aeat_state == "sent_w_errors" else "register"
629
+ )
630
+ rec.verifactu_registration_date = datetime.now()
631
+ rec._generate_verifactu_chaining(entry_type=entry_type)
@@ -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
+ }