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.
- odoo/addons/l10n_es_verifactu_oca/README.rst +195 -0
- odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
- odoo/addons/l10n_es_verifactu_oca/__manifest__.py +50 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +13 -0
- odoo/addons/l10n_es_verifactu_oca/data/l10n.es.aeat.map.tax.line.tax.csv +50 -0
- odoo/addons/l10n_es_verifactu_oca/data/mail_activity_data.xml +11 -0
- odoo/addons/l10n_es_verifactu_oca/data/neutralize.sql +2 -0
- odoo/addons/l10n_es_verifactu_oca/data/template/account.fiscal.position-es_common.csv +27 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.csv +2 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.line.csv +8 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +205 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
- odoo/addons/l10n_es_verifactu_oca/hooks.py +43 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1682 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1640 -0
- odoo/addons/l10n_es_verifactu_oca/migrations/18.0.1.1.0/pre-migration.py +25 -0
- odoo/addons/l10n_es_verifactu_oca/models/__init__.py +15 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_chart_template.py +17 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +34 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_move.py +631 -0
- odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
- odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
- odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +26 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +37 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +398 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +116 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +47 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +68 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +485 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +26 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.md +27 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.md +18 -0
- odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.md +1 -0
- odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.md +6 -0
- odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.md +30 -0
- odoo/addons/l10n_es_verifactu_oca/readme/USAGE.md +3 -0
- odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +23 -0
- odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
- odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
- odoo/addons/l10n_es_verifactu_oca/static/description/index.html +551 -0
- odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +2 -0
- odoo/addons/l10n_es_verifactu_oca/tests/common.py +281 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_accepted_with_errors.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_incorrect.json +37 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_with_errors.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_correct.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_duplicated.json +43 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_incorrect.json +37 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +506 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +348 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +29 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +22 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +237 -0
- odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
- odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +53 -0
- odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +45 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +46 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +134 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +127 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +16 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +54 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +43 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +2 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard.py +24 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard_view.xml +35 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/METADATA +213 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/RECORD +78 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/WHEEL +5 -0
- 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
|
+
}
|