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,47 @@
1
+ # Copyright 2025 ForgeFlow S.L.
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3
+
4
+ from odoo import fields, models
5
+
6
+ from ..models.verifactu_invoice_entry import VERIFACTU_SEND_STATES
7
+
8
+
9
+ class VerifactuInvoiceEntryResponseLine(models.Model):
10
+ _name = "verifactu.invoice.entry.response.line"
11
+ _description = "VERI*FACTU send log"
12
+ _order = "id desc"
13
+
14
+ entry_id = fields.Many2one(comodel_name="verifactu.invoice.entry", required=True)
15
+ entry_response_id = fields.Many2one("verifactu.invoice.entry.response")
16
+ model = fields.Char(readonly=True)
17
+ document_id = fields.Many2oneReference(
18
+ string="Document", model_field="model", readonly=True, index=True
19
+ )
20
+ response = fields.Text()
21
+ send_state = fields.Selection(
22
+ selection=VERIFACTU_SEND_STATES,
23
+ string="VERI*FACTU send state",
24
+ default="not_sent",
25
+ readonly=True,
26
+ copy=False,
27
+ help="Indicates the state of this document in relation with the "
28
+ "presentation to VERI*FACTU.",
29
+ )
30
+ verifactu_csv = fields.Text(related="entry_response_id.verifactu_csv")
31
+ error_code = fields.Char()
32
+ document_name = fields.Char(related="entry_id.document_name", readonly=True)
33
+ is_cancellation = fields.Boolean(
34
+ string="Is cancellation",
35
+ compute="_compute_is_cancellation",
36
+ readonly=True,
37
+ help="Indicates whether this response line is related to a "
38
+ "cancellation request.",
39
+ )
40
+
41
+ @property
42
+ def document(self):
43
+ return self.env[self.model].browse(self.document_id).exists()
44
+
45
+ def _compute_is_cancellation(self):
46
+ for rec in self:
47
+ rec.is_cancellation = rec.entry_id.entry_type == "cancel"
@@ -0,0 +1,68 @@
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.html).
3
+ from odoo import _, api, exceptions, fields, models
4
+
5
+
6
+ class AeatVerifactuMap(models.Model):
7
+ _name = "verifactu.map"
8
+ _description = "VERI*FACTU mapping"
9
+
10
+ name = fields.Char(string="Model", required=True)
11
+ date_from = fields.Date()
12
+ date_to = fields.Date()
13
+ map_lines = fields.One2many(
14
+ comodel_name="verifactu.map.line",
15
+ inverse_name="verifactu_map_id",
16
+ string="Lines",
17
+ )
18
+
19
+ @api.constrains("date_from", "date_to")
20
+ def _unique_date_range(self):
21
+ for record in self:
22
+ record._unique_date_range_one()
23
+
24
+ def _unique_date_range_one(self):
25
+ # Based in l10n_es_aeat module
26
+ domain = [("id", "!=", self.id)]
27
+ if self.date_from and self.date_to:
28
+ domain += [
29
+ "|",
30
+ "&",
31
+ ("date_from", "<=", self.date_to),
32
+ ("date_from", ">=", self.date_from),
33
+ "|",
34
+ "&",
35
+ ("date_to", "<=", self.date_to),
36
+ ("date_to", ">=", self.date_from),
37
+ "|",
38
+ "&",
39
+ ("date_from", "=", False),
40
+ ("date_to", ">=", self.date_from),
41
+ "|",
42
+ "&",
43
+ ("date_to", "=", False),
44
+ ("date_from", "<=", self.date_to),
45
+ ]
46
+ elif self.date_from:
47
+ domain += [("date_to", ">=", self.date_from)]
48
+ elif self.date_to:
49
+ domain += [("date_from", "<=", self.date_to)]
50
+ date_lst = self.search(domain)
51
+ if date_lst:
52
+ raise exceptions.UserError(
53
+ _("Error! The dates of the record overlap with an existing " "record.")
54
+ )
55
+
56
+
57
+ class AeatVerifactuMapLines(models.Model):
58
+ _name = "verifactu.map.line"
59
+ _description = "VERI*FACTU mapping line"
60
+
61
+ code = fields.Char(required=True)
62
+ name = fields.Char()
63
+ tax_xmlid_ids = fields.Many2many(
64
+ comodel_name="l10n.es.aeat.map.tax.line.tax", string="Taxes templates"
65
+ )
66
+ verifactu_map_id = fields.Many2one(
67
+ comodel_name="verifactu.map", string="Parent mapping", ondelete="cascade"
68
+ )
@@ -0,0 +1,485 @@
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
+ import base64
7
+ import io
8
+ import json
9
+ from hashlib import sha256
10
+ from urllib.parse import urlencode
11
+
12
+ import psycopg2
13
+ import qrcode
14
+
15
+ from odoo import _, api, fields, models
16
+ from odoo.exceptions import UserError
17
+ from odoo.tools.float_utils import float_compare
18
+
19
+ from odoo.addons.l10n_es_aeat.models.aeat_mixin import round_by_keys
20
+
21
+ VERIFACTU_VERSION = 1.0
22
+ VERIFACTU_DATE_FORMAT = "%d-%m-%Y"
23
+ VERIFACTU_MACRODATA_LIMIT = 100000000.0
24
+
25
+ VERIFACTU_EXTRA_AEAT_STATES = [
26
+ ("incorrect", "Incorrect"),
27
+ ("cancel", "Cancelled in VERI*FACTU"),
28
+ ("cancel_w_errors", "Cancelled in VERI*FACTU with Errors"),
29
+ ("cancel_incorrect", "Incorrect cancellation in VERI*FACTU"),
30
+ ]
31
+
32
+
33
+ class VerifactuMixin(models.AbstractModel):
34
+ _name = "verifactu.mixin"
35
+ _inherit = "aeat.mixin"
36
+ _description = "VERI*FACTU mixin"
37
+
38
+ verifactu_enabled = fields.Boolean(
39
+ string="VERI*FACTU enabled",
40
+ compute="_compute_verifactu_enabled",
41
+ search="_search_verifactu_enabled",
42
+ )
43
+ verifactu_hash_string = fields.Char(
44
+ string="VERI*FACTU hash string", copy=False, tracking=True
45
+ )
46
+ verifactu_hash = fields.Char(string="VERI*FACTU hash", copy=False, tracking=True)
47
+ verifactu_refund_type = fields.Selection(
48
+ string="VERI*FACTU refund type",
49
+ selection=[
50
+ # ('S', 'By substitution'), - TODO: no está soportado por el momento
51
+ ("I", "By differences"),
52
+ ],
53
+ compute="_compute_verifactu_refund_type",
54
+ store=True,
55
+ readonly=False,
56
+ )
57
+ verifactu_description = fields.Text(string="VERI*FACTU description", copy=False)
58
+ verifactu_macrodata = fields.Boolean(
59
+ string="VERI*FACTU macrodata?",
60
+ help="Check to confirm that the document has an absolute amount "
61
+ "greater o equal to 100 000 000,00 euros.",
62
+ compute="_compute_verifactu_macrodata",
63
+ )
64
+ verifactu_csv = fields.Char(string="VERI*FACTU CSV", copy=False, readonly=True)
65
+ verifactu_return = fields.Text(
66
+ string="VERI*FACTU return", copy=False, readonly=True
67
+ )
68
+ verifactu_registration_date = fields.Datetime(
69
+ string="VERI*FACTU registration date", copy=False
70
+ )
71
+ verifactu_registration_key = fields.Many2one(
72
+ string="VERI*FACTU registration key",
73
+ comodel_name="verifactu.registration.key",
74
+ compute="_compute_verifactu_registration_key",
75
+ store=True,
76
+ readonly=False,
77
+ )
78
+ verifactu_tax_key = fields.Selection(
79
+ string="VERI*FACTU tax key",
80
+ selection="_get_verifactu_tax_keys",
81
+ compute="_compute_verifactu_tax_key",
82
+ store=True,
83
+ readonly=False,
84
+ )
85
+ verifactu_registration_key_code = fields.Char(
86
+ string="VERI*FACTU key code", compute="_compute_verifactu_registration_key_code"
87
+ )
88
+ verifactu_qr_url = fields.Char(
89
+ string="VERI*FACTU URL", compute="_compute_verifactu_qr_url"
90
+ )
91
+ verifactu_qr = fields.Binary(
92
+ string="VERI*FACTU QR", compute="_compute_verifactu_qr"
93
+ )
94
+ verifactu_send_date = fields.Datetime(
95
+ string="VERI*FACTU send date", index=True, copy=False
96
+ )
97
+ verifactu_invoice_entry_ids = fields.One2many(
98
+ comodel_name="verifactu.invoice.entry",
99
+ inverse_name="document_id",
100
+ domain=lambda doc: [("model", "=", doc._name)],
101
+ string="VERI*FACTU invoice entries",
102
+ readonly=True,
103
+ copy=False,
104
+ )
105
+ verifactu_response_line_ids = fields.One2many(
106
+ comodel_name="verifactu.invoice.entry.response.line",
107
+ inverse_name="document_id",
108
+ domain=lambda doc: [("model", "=", doc._name)],
109
+ string="VERI*FACTU response lines",
110
+ readonly=True,
111
+ copy=False,
112
+ )
113
+ last_verifactu_invoice_entry_id = fields.Many2one(
114
+ comodel_name="verifactu.invoice.entry",
115
+ string="VERI*FACTU last invoice entry",
116
+ readonly=True,
117
+ copy=False,
118
+ )
119
+ last_verifactu_response_line_id = fields.Many2one(
120
+ comodel_name="verifactu.invoice.entry.response.line",
121
+ string="VERI*FACTU response line",
122
+ readonly=True,
123
+ copy=False,
124
+ )
125
+ aeat_state = fields.Selection(
126
+ selection_add=VERIFACTU_EXTRA_AEAT_STATES,
127
+ compute="_compute_aeat_state",
128
+ store=True,
129
+ copy=False,
130
+ )
131
+ verifactu_cancel_reason = fields.Char(
132
+ string="VERI*FACTU cancel reason",
133
+ help="Optional internal reason to cancel this invoice"
134
+ "in VERI*FACTU. This reason is not sent to VERI*FACTU",
135
+ )
136
+
137
+ @api.depends("last_verifactu_response_line_id.send_state")
138
+ def _compute_aeat_state(self):
139
+ for document in self:
140
+ if document.verifactu_enabled and document.last_verifactu_response_line_id:
141
+ document.aeat_state = (
142
+ document.last_verifactu_response_line_id.send_state
143
+ )
144
+
145
+ @api.model
146
+ def _get_verifactu_reference_models(self):
147
+ """This method is used to define the models that can be used as
148
+ previous documents in the VERI*FACTU mixin.
149
+ """
150
+ return ["account.move"]
151
+
152
+ def _compute_verifactu_enabled(self):
153
+ raise NotImplementedError
154
+
155
+ def _compute_verifactu_macrodata(self):
156
+ for document in self:
157
+ amount_total = document._get_verifactu_taxes_and_total()[2]
158
+ document.verifactu_macrodata = (
159
+ float_compare(
160
+ abs(amount_total), VERIFACTU_MACRODATA_LIMIT, precision_digits=2
161
+ )
162
+ >= 0
163
+ )
164
+
165
+ def _compute_verifactu_qr_url(self):
166
+ """Returns the URL to be used in the QR code. A sample URL would be (urlencoded):
167
+ https://prewww2.aeat.es/wlpl/TIKECONT/ValidarQR?nif=89890001K&numserie=12345678%26G33&fecha=01-01-2024&importe=241.4
168
+ """ # noqa: E501
169
+ for record in self:
170
+ # FIXME: Not be hard-coded
171
+ agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
172
+ if record.company_id.verifactu_test:
173
+ qr_base_url = agency.verifactu_qr_base_url_test_address
174
+ else:
175
+ qr_base_url = agency.verifactu_qr_base_url
176
+ qr_values = record._get_verifactu_qr_values()
177
+ # Check all values are ASCII between 32 and 126
178
+ for value in qr_values.values():
179
+ try:
180
+ str(value).encode("ascii")
181
+ except UnicodeEncodeError as uee:
182
+ raise UserError(
183
+ _("QR URL value '{}' is not ASCII").format(value)
184
+ ) from uee
185
+ # Build QR URL
186
+ qr_url = urlencode(qr_values, encoding="utf-8")
187
+ record.verifactu_qr_url = f"{qr_base_url}?{qr_url}"
188
+
189
+ def _compute_verifactu_qr(self):
190
+ for record in self:
191
+ if record.state != "posted" or not record.verifactu_enabled:
192
+ record.verifactu_qr = False
193
+ continue
194
+ qr = qrcode.QRCode(
195
+ border=0, error_correction=qrcode.constants.ERROR_CORRECT_M
196
+ )
197
+ qr.add_data(record.verifactu_qr_url)
198
+ qr.make()
199
+ img = qr.make_image()
200
+ with io.BytesIO() as temp:
201
+ img.save(temp, format="PNG")
202
+ record.verifactu_qr = base64.b64encode(temp.getvalue())
203
+
204
+ def _compute_verifactu_registration_key(self):
205
+ raise NotImplementedError()
206
+
207
+ def _compute_verifactu_tax_key(self):
208
+ raise NotImplementedError()
209
+
210
+ @api.depends("verifactu_registration_key")
211
+ def _compute_verifactu_registration_key_code(self):
212
+ for record in self:
213
+ record.verifactu_registration_key_code = (
214
+ record.verifactu_registration_key.code
215
+ )
216
+
217
+ @api.model
218
+ def _search_verifactu_enabled(self, operator, value):
219
+ if operator not in ("=", "!="):
220
+ raise ValueError(_("Unsupported search operator"))
221
+ return [("company_id.verifactu_enabled", operator, value)]
222
+
223
+ def _get_verifactu_qr_values(self):
224
+ raise NotImplementedError
225
+
226
+ @api.model
227
+ def _get_verifactu_tax_keys(self):
228
+ return self.env["account.fiscal.position"]._get_verifactu_tax_keys()
229
+
230
+ def _connect_verifactu_params_aeat(self, mapping_key):
231
+ self.ensure_one()
232
+ agency = self.company_id.tax_agency_id
233
+ if not agency:
234
+ # We use spanish agency by default to keep old behavior with
235
+ # ir.config parameters. In the future it might be good to reinforce
236
+ # to explicitly set a tax agency in the company by raising an error
237
+ # here.
238
+ agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
239
+ return agency._connect_params_verifactu(self.company_id)
240
+
241
+ def _get_verifactu_invoice_dict(self, cancel=False):
242
+ self.ensure_one()
243
+ inv_dict = {}
244
+ mapping_key = self._get_mapping_key()
245
+ if mapping_key in ["out_invoice", "out_refund"]:
246
+ if cancel:
247
+ inv_dict = self._get_verifactu_cancel_invoice_dict_out()
248
+ else:
249
+ inv_dict = self._get_verifactu_invoice_dict_out()
250
+ else:
251
+ raise NotImplementedError
252
+ round_by_keys(
253
+ inv_dict,
254
+ [
255
+ "BaseImponibleOimporteNoSujeto",
256
+ "CuotaRepercutida",
257
+ "TipoRecargoEquivalencia",
258
+ "CuotaRecargoEquivalencia",
259
+ "CuotaTotal",
260
+ "ImporteTotal",
261
+ "BaseRectificada",
262
+ "CuotaRectificada",
263
+ ],
264
+ )
265
+ return inv_dict
266
+
267
+ def _get_verifactu_developer_dict(self):
268
+ """Datos del desarrollador del sistema informático."""
269
+ if not self.company_id.verifactu_developer_id:
270
+ raise UserError(
271
+ _("Please, configure the VERI*FACTU developer in your company")
272
+ )
273
+ developer = self.company_id.verifactu_developer_id
274
+ chaining = self._get_verifactu_chaining()
275
+ verifactu_companies = (
276
+ self.env["res.company"]
277
+ .sudo()
278
+ .search_count([("verifactu_enabled", "=", True)])
279
+ )
280
+ return {
281
+ "NombreRazon": developer.name,
282
+ "NIF": developer.vat,
283
+ "NombreSistemaInformatico": developer.sif_name,
284
+ "IdSistemaInformatico": chaining.sif_id,
285
+ "Version": developer.version,
286
+ "NumeroInstalacion": chaining.installation_number,
287
+ "TipoUsoPosibleSoloVerifactu": "S",
288
+ "TipoUsoPosibleMultiOT": "S",
289
+ "IndicadorMultiplesOT": "S" if verifactu_companies > 1 else "N",
290
+ "IDOtro": {
291
+ "IDType": "",
292
+ "ID": "",
293
+ },
294
+ }
295
+
296
+ def _get_verifactu_chaining_invoice_dict(self):
297
+ raise NotImplementedError
298
+
299
+ def _aeat_check_exceptions(self):
300
+ """Inheritable method for exception control when sending VERI*FACTU invoices."""
301
+ res = super()._aeat_check_exceptions()
302
+ if self.company_id.verifactu_enabled and not self.verifactu_enabled:
303
+ raise UserError(_("This invoice is not VERI*FACTU enabled."))
304
+ return res
305
+
306
+ def _get_verifactu_date(self, date):
307
+ datetimeobject = fields.Date.to_date(date)
308
+ return datetimeobject.strftime(VERIFACTU_DATE_FORMAT)
309
+
310
+ def _get_verifactu_hash_string(self, cancel=False):
311
+ raise NotImplementedError
312
+
313
+ def _get_verifactu_chaining(self):
314
+ return NotImplementedError
315
+
316
+ def _generate_verifactu_chaining(self, entry_type=False):
317
+ """Generate VERI*FACTU invoice entry for company-wide chaining."""
318
+ self.ensure_one()
319
+ chaining = self._get_verifactu_chaining()
320
+ chaining.flush_recordset(["last_verifactu_invoice_entry_id"])
321
+ cancel = entry_type and entry_type == "cancel"
322
+ try:
323
+ with self.env.cr.savepoint():
324
+ self.env.cr.execute(
325
+ f"SELECT last_verifactu_invoice_entry_id FROM {chaining._table}"
326
+ " WHERE id = %s FOR UPDATE NOWAIT",
327
+ [chaining.id],
328
+ )
329
+ result = self.env.cr.fetchone()
330
+ previous_invoice_entry_id = result[0] if result and result[0] else False
331
+ invoice_vals = {
332
+ "verifactu_chaining_id": chaining.id,
333
+ "model": self._name,
334
+ "document_id": self.id,
335
+ "document_name": self.name,
336
+ "previous_invoice_entry_id": previous_invoice_entry_id,
337
+ "company_id": self.company_id.id,
338
+ "document_hash": "",
339
+ }
340
+ if entry_type:
341
+ invoice_vals["entry_type"] = entry_type
342
+ invoice_entry = self.env["verifactu.invoice.entry"].create(invoice_vals)
343
+ self.last_verifactu_invoice_entry_id = invoice_entry
344
+ verifactu_hash_values = self._get_verifactu_hash_string(cancel=cancel)
345
+ self.verifactu_hash_string = verifactu_hash_values
346
+ hash_string = sha256(verifactu_hash_values.encode("utf-8"))
347
+ self.verifactu_hash = hash_string.hexdigest().upper()
348
+ # Generate JSON data for AEAT
349
+ aeat_json_data = ""
350
+ try:
351
+ inv_dict = self._get_verifactu_invoice_dict(cancel=cancel)
352
+ aeat_json_data = json.dumps(inv_dict, indent=4)
353
+ except Exception:
354
+ # If JSON generation fails, store empty string
355
+ aeat_json_data = ""
356
+ invoice_entry.document_hash = hash_string.hexdigest().upper()
357
+ invoice_entry.aeat_json_data = aeat_json_data
358
+ self.env.cr.execute(
359
+ f"UPDATE {chaining._table} "
360
+ "SET last_verifactu_invoice_entry_id = %s WHERE id = %s",
361
+ [invoice_entry.id, chaining.id],
362
+ )
363
+ chaining.invalidate_recordset(["last_verifactu_invoice_entry_id"])
364
+ except psycopg2.OperationalError as err:
365
+ if err.pgcode == "55P03": # could not obtain the lock
366
+ raise UserError(
367
+ _(
368
+ "Could not obtain last document sent to VERI*FACTU for "
369
+ "chaining %s.",
370
+ chaining.name,
371
+ )
372
+ ) from err
373
+ raise
374
+
375
+ def _get_verifactu_document_type(self):
376
+ raise NotImplementedError()
377
+
378
+ def _get_verifactu_description(self):
379
+ raise NotImplementedError()
380
+
381
+ def _get_verifactu_taxes_and_total(self):
382
+ raise NotImplementedError
383
+
384
+ def _get_verifactu_version(self):
385
+ return VERIFACTU_VERSION
386
+
387
+ def _get_verifactu_receiver_dict(self):
388
+ raise NotImplementedError
389
+
390
+ def _compute_verifactu_refund_type(self):
391
+ self.verifactu_refund_type = False
392
+
393
+ def _get_verifactu_accepted_tax_agencies(self):
394
+ return ["l10n_es_aeat.aeat_tax_agency_spain"]
395
+
396
+ def _check_verifactu_configuration(self, suffixes=None):
397
+ prefix = _("The invoice %s cannot be sent to VERI*FACTU because:")
398
+ if not suffixes:
399
+ suffixes = []
400
+ if not self._get_verifactu_chaining():
401
+ suffixes.append(
402
+ _("- Your company does not have a VERI*FACTU chaining configured.")
403
+ )
404
+ if not self.company_id.tax_agency_id:
405
+ suffixes.append(_("- Your company does not have a tax agency configured."))
406
+ if (
407
+ self.company_id.tax_agency_id.get_external_id
408
+ in self._get_verifactu_accepted_tax_agencies()
409
+ ):
410
+ suffixes.append(_("- Your company's tax agency is not supported."))
411
+ if not self.company_id.verifactu_developer_id:
412
+ suffixes.append(
413
+ _("- Your company does not have a VERI*FACTU developer configured.")
414
+ )
415
+ if not self.company_id.country_code or self.company_id.country_code != "ES":
416
+ suffixes.append(_("Your company is not registered in Spain."))
417
+ if suffixes:
418
+ raise UserError((prefix + "\n".join(suffixes)) % self[self._rec_name])
419
+
420
+ @api.model
421
+ def _get_verifactu_map(self, date):
422
+ return (
423
+ self.env["verifactu.map"]
424
+ .sudo()
425
+ .with_context(active_test=False)
426
+ .search(
427
+ [
428
+ "|",
429
+ ("date_from", "<=", date),
430
+ ("date_from", "=", False),
431
+ "|",
432
+ ("date_to", ">=", date),
433
+ ("date_to", "=", False),
434
+ ],
435
+ limit=1,
436
+ )
437
+ )
438
+
439
+ @api.model
440
+ def _get_verifactu_taxes_map(self, codes, date):
441
+ """Return the codes that correspond to verifactu map line codes.
442
+
443
+ :param codes: List of code strings to get the mapping.
444
+ :param date: Date to map
445
+ :return: Recordset with the corresponding codes
446
+ """
447
+ verifactu_map = self._get_verifactu_map(date)
448
+ tax_templates = verifactu_map.map_lines.filtered(
449
+ lambda x: x.code in codes
450
+ ).tax_xmlid_ids
451
+ mapped_taxes = self.env["account.tax"]
452
+ for template in tax_templates:
453
+ tax_id = self.company_id._get_tax_id_from_xmlid(template.name)
454
+ mapped_taxes |= self.env["account.tax"].browse(tax_id)
455
+ return mapped_taxes
456
+
457
+ def _raise_exception_verifactu(self, field_name):
458
+ raise UserError(
459
+ _(
460
+ "You cannot change the %s of document "
461
+ "already registered at VERI*FACTU. You must cancel the "
462
+ "document and create a new one with the correct value."
463
+ )
464
+ % field_name
465
+ )
466
+
467
+ @api.model
468
+ def _get_verifactu_batch(self):
469
+ try:
470
+ return int(
471
+ self.env["ir.config_parameter"]
472
+ .sudo()
473
+ .get_param("l10n_es_verifactu_oca.verifactu_batch", "1000")
474
+ )
475
+ except ValueError as e:
476
+ raise UserError(
477
+ _(
478
+ "The value in l10n_es_verifactu_oca.verifactu_batch "
479
+ "system parameter must be an integer. Please, check the "
480
+ "value of the parameter."
481
+ )
482
+ ) from e
483
+
484
+ def cancel_verifactu(self):
485
+ raise NotImplementedError
@@ -0,0 +1,26 @@
1
+ # Copyright 2024 Aures TIC - Almudena de La Puente
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 AeatVerifactuMappingRegistrationKeys(models.Model):
8
+ _name = "verifactu.registration.key"
9
+ _description = "VERI*FACTU registration key"
10
+
11
+ code = fields.Char(required=True, size=2)
12
+ name = fields.Char(required=True)
13
+ verifactu_tax_key = fields.Selection(
14
+ selection="_get_verifactu_tax_keys",
15
+ string="VERI*FACTU tax key",
16
+ required=True,
17
+ )
18
+
19
+ @api.depends("name", "code")
20
+ def _compute_display_name(self):
21
+ for record in self:
22
+ record.display_name = f"[{record.code}]-{record.name}"
23
+
24
+ @api.model
25
+ def _get_verifactu_tax_keys(self):
26
+ return self.env["account.fiscal.position"]._get_verifactu_tax_keys()
@@ -0,0 +1,27 @@
1
+ Para configurar este módulo es necesario:
2
+
3
+ 1. Acceder a Facturación/Contabilidad -\> Configuración -\> AEAT -\>
4
+ Agencia Tributaria, podrás consultar las URLs del servicio SOAP de
5
+ Hacienda. Estas URLs pueden cambiar según comunidades
6
+ 2. El certificado enviado por la FMNT es en formato p12, este
7
+ certificado no se puede usar directamente con Zeep. Accede a
8
+ Facturación/Contabilidad -\> Configuración -\> AEAT -\> Certificados
9
+ AEAT, y allí podrás: Subir el certificado p12 y extraer las claves
10
+ públicas y privadas con el botón "Obtener claves"
11
+ 3. Debes tener en cuenta que los certificados se alojan en una carpeta
12
+ accesible por la instalación de Odoo.
13
+ 4. Completar los datos de desarrollador y del encadenamiento a nivel de
14
+ compañía en la pestaña de VERI\*FACTU.
15
+
16
+ En caso de que la obtención de claves no funcione y uses Linux, cuentas
17
+ con los siguientes comandos para tratar de solucionarlo:
18
+
19
+ - Clave pública: "openssl pkcs12 -in Certificado.p12 -nokeys -out
20
+ publicCert.crt -nodes"
21
+ - Clave privada: "openssl pkcs12 -in Certificado.p12 -nocerts -out
22
+ privateKey.pem -nodes"
23
+
24
+ 1. Establecer en las posiciones fiscales la clave de impuestos y la
25
+ clave de registro VERI\*FACTU.
26
+ 2. Para aplicar las claves ejecute el asistente de actualización del
27
+ módulo accountchart_update.
@@ -0,0 +1,18 @@
1
+ - Aures TIC:
2
+ - Jose Zambudio
3
+ - Almudena de La Puente
4
+ - Anna Martínez
5
+ - ForgeFlow S.L.:
6
+ - Laura Cazorla
7
+ - Andreu Orensanz
8
+ - Jordi Ballester
9
+ - Ozono multimedia:
10
+ - Iván Antón
11
+ - SDi:
12
+ - Fernando La Chica
13
+ - Process control:
14
+ - Jorge Luis López
15
+ - Tecnativa:
16
+ - Pedro M. Baeza
17
+ - Factor Libre S.L.:
18
+ - Luis J. Salvatierra
@@ -0,0 +1 @@
1
+ Módulo para la presentación inmediata de la facturación.
@@ -0,0 +1,6 @@
1
+ Para instalar esté módulo necesita:
2
+
3
+ 1. Libreria Python Zeep, se puede instalar con el comando 'pip install
4
+ zeep'
5
+ 2. Libreria Python Requests, se puede instalar con el comando 'pip
6
+ install requests'