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