odoo-addon-l10n-es-aeat-sii-oca 16.0.1.3.2.1__py3-none-any.whl → 16.0.1.5.0.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_aeat_sii_oca/README.rst +1 -1
- odoo/addons/l10n_es_aeat_sii_oca/__manifest__.py +1 -1
- odoo/addons/l10n_es_aeat_sii_oca/data/aeat_sii_queue_job.xml +3 -3
- odoo/addons/l10n_es_aeat_sii_oca/i18n/bg.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/ca.po +99 -28
- odoo/addons/l10n_es_aeat_sii_oca/i18n/cs.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/de.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/es.po +122 -36
- odoo/addons/l10n_es_aeat_sii_oca/i18n/es_CO.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/es_CR.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/eu.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/fr.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/gl.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/hr.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/l10n_es_aeat_sii_oca.pot +62 -16
- odoo/addons/l10n_es_aeat_sii_oca/i18n/nl.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/pl.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/pt.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/pt_BR.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/ru.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/sl.po +61 -18
- odoo/addons/l10n_es_aeat_sii_oca/i18n/sv.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/tr.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/i18n/vi.po +61 -15
- odoo/addons/l10n_es_aeat_sii_oca/migrations/16.0.1.4.0/post-migration.py +18 -0
- odoo/addons/l10n_es_aeat_sii_oca/models/__init__.py +1 -0
- odoo/addons/l10n_es_aeat_sii_oca/models/account_move.py +126 -693
- odoo/addons/l10n_es_aeat_sii_oca/models/sii_mixin.py +915 -0
- odoo/addons/l10n_es_aeat_sii_oca/static/description/index.html +1 -1
- odoo/addons/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py +38 -43
- {odoo_addon_l10n_es_aeat_sii_oca-16.0.1.3.2.1.dist-info → odoo_addon_l10n_es_aeat_sii_oca-16.0.1.5.0.1.dist-info}/METADATA +2 -2
- {odoo_addon_l10n_es_aeat_sii_oca-16.0.1.3.2.1.dist-info → odoo_addon_l10n_es_aeat_sii_oca-16.0.1.5.0.1.dist-info}/RECORD +34 -32
- {odoo_addon_l10n_es_aeat_sii_oca-16.0.1.3.2.1.dist-info → odoo_addon_l10n_es_aeat_sii_oca-16.0.1.5.0.1.dist-info}/WHEEL +0 -0
- {odoo_addon_l10n_es_aeat_sii_oca-16.0.1.3.2.1.dist-info → odoo_addon_l10n_es_aeat_sii_oca-16.0.1.5.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,915 @@
|
|
1
|
+
# Copyright 2021 Tecnativa - João Marques
|
2
|
+
# Copyright 2022 ForgeFlow - Lois Rilo
|
3
|
+
# Copyright 2011-2023 Tecnativa - Pedro M. Baeza
|
4
|
+
# Copyright 2023 Aures Tic - Almudena de la Puente <almudena@aurestic.es>
|
5
|
+
# Copyright 2023 Aures Tic - Jose Zambudio <jose@aurestic.es>
|
6
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
7
|
+
import json
|
8
|
+
import logging
|
9
|
+
|
10
|
+
from requests import Session
|
11
|
+
|
12
|
+
from odoo import _, api, exceptions, fields, models
|
13
|
+
from odoo.exceptions import UserError, ValidationError
|
14
|
+
from odoo.modules.registry import Registry
|
15
|
+
from odoo.tools.float_utils import float_compare
|
16
|
+
|
17
|
+
_logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
try:
|
20
|
+
from zeep import Client
|
21
|
+
from zeep.plugins import HistoryPlugin
|
22
|
+
from zeep.transports import Transport
|
23
|
+
except (ImportError, IOError) as err:
|
24
|
+
_logger.debug(err)
|
25
|
+
|
26
|
+
SII_STATES = [
|
27
|
+
("not_sent", "Not sent"),
|
28
|
+
("sent", "Sent"),
|
29
|
+
("sent_w_errors", "Accepted with errors"),
|
30
|
+
("sent_modified", "Registered in SII but last modifications not sent"),
|
31
|
+
("cancelled", "Cancelled"),
|
32
|
+
("cancelled_modified", "Cancelled in SII but last modifications not sent"),
|
33
|
+
]
|
34
|
+
SII_VERSION = "1.1"
|
35
|
+
SII_MACRODATA_LIMIT = 100000000.0
|
36
|
+
SII_DATE_FORMAT = "%d-%m-%Y"
|
37
|
+
|
38
|
+
|
39
|
+
def round_by_keys(elem, search_keys, prec=2):
|
40
|
+
"""This uses ``round`` method directly as if has been tested that Odoo's
|
41
|
+
``float_round`` still returns incorrect amounts for certain values. Try
|
42
|
+
3 units x 3,77 €/unit with 10% tax and you will be hit by the error
|
43
|
+
(on regular x86 architectures)."""
|
44
|
+
if isinstance(elem, dict):
|
45
|
+
for key, value in elem.items():
|
46
|
+
if key in search_keys:
|
47
|
+
elem[key] = round(elem[key], prec)
|
48
|
+
else:
|
49
|
+
round_by_keys(value, search_keys)
|
50
|
+
elif isinstance(elem, list):
|
51
|
+
for value in elem:
|
52
|
+
round_by_keys(value, search_keys)
|
53
|
+
|
54
|
+
|
55
|
+
class SiiMixin(models.AbstractModel):
|
56
|
+
_name = "sii.mixin"
|
57
|
+
_description = "SII Mixin"
|
58
|
+
|
59
|
+
company_id = fields.Many2one(
|
60
|
+
comodel_name="res.company",
|
61
|
+
string="Company",
|
62
|
+
)
|
63
|
+
sii_description = fields.Text(
|
64
|
+
string="SII computed description",
|
65
|
+
compute="_compute_sii_description",
|
66
|
+
default="/",
|
67
|
+
store=True,
|
68
|
+
readonly=False,
|
69
|
+
copy=False,
|
70
|
+
)
|
71
|
+
sii_state = fields.Selection(
|
72
|
+
selection=SII_STATES,
|
73
|
+
string="SII send state",
|
74
|
+
default="not_sent",
|
75
|
+
readonly=True,
|
76
|
+
copy=False,
|
77
|
+
help="Indicates the state of this document in relation with the "
|
78
|
+
"presentation at the SII",
|
79
|
+
)
|
80
|
+
sii_csv = fields.Char(string="SII CSV", copy=False, readonly=True)
|
81
|
+
sii_return = fields.Text(string="SII Return", copy=False, readonly=True)
|
82
|
+
sii_header_sent = fields.Text(
|
83
|
+
string="SII last header sent",
|
84
|
+
copy=False,
|
85
|
+
readonly=True,
|
86
|
+
)
|
87
|
+
sii_content_sent = fields.Text(
|
88
|
+
string="SII last content sent",
|
89
|
+
copy=False,
|
90
|
+
readonly=True,
|
91
|
+
)
|
92
|
+
sii_send_error = fields.Text(
|
93
|
+
string="SII Send Error",
|
94
|
+
readonly=True,
|
95
|
+
copy=False,
|
96
|
+
)
|
97
|
+
sii_send_failed = fields.Boolean(
|
98
|
+
string="SII send failed",
|
99
|
+
copy=False,
|
100
|
+
help="Indicates that the last attempt to communicate this document to "
|
101
|
+
"the SII has failed. See SII return for details",
|
102
|
+
)
|
103
|
+
sii_refund_type = fields.Selection(
|
104
|
+
selection=[
|
105
|
+
# ('S', 'By substitution'), - Removed as not fully supported
|
106
|
+
("I", "By differences"),
|
107
|
+
],
|
108
|
+
string="SII Refund Type",
|
109
|
+
compute="_compute_sii_refund_type",
|
110
|
+
store=True,
|
111
|
+
readonly=False,
|
112
|
+
)
|
113
|
+
sii_account_registration_date = fields.Date(
|
114
|
+
string="SII account registration date",
|
115
|
+
readonly=True,
|
116
|
+
copy=False,
|
117
|
+
help="Indicates the account registration date set at the SII, which "
|
118
|
+
"must be the date when the document is recorded in the system and "
|
119
|
+
"is independent of the date of the accounting entry of the "
|
120
|
+
"document",
|
121
|
+
)
|
122
|
+
sii_registration_key_domain = fields.Char(
|
123
|
+
compute="_compute_sii_registration_key_domain",
|
124
|
+
string="SII registration key domain",
|
125
|
+
)
|
126
|
+
sii_registration_key = fields.Many2one(
|
127
|
+
comodel_name="aeat.sii.mapping.registration.keys",
|
128
|
+
string="SII registration key",
|
129
|
+
compute="_compute_sii_registration_key",
|
130
|
+
store=True,
|
131
|
+
readonly=False,
|
132
|
+
# required=True, This is not set as required here to avoid the
|
133
|
+
# set not null constraint warning
|
134
|
+
)
|
135
|
+
sii_registration_key_code = fields.Char(
|
136
|
+
compute="_compute_sii_registration_key_code",
|
137
|
+
readonly=True,
|
138
|
+
string="SII Code",
|
139
|
+
)
|
140
|
+
sii_enabled = fields.Boolean(
|
141
|
+
string="Enable SII",
|
142
|
+
compute="_compute_sii_enabled",
|
143
|
+
)
|
144
|
+
sii_macrodata = fields.Boolean(
|
145
|
+
string="MacroData",
|
146
|
+
help="Check to confirm that the document has an absolute amount "
|
147
|
+
"greater o equal to 100 000 000,00 euros.",
|
148
|
+
compute="_compute_macrodata",
|
149
|
+
)
|
150
|
+
|
151
|
+
def _compute_sii_refund_type(self):
|
152
|
+
self.sii_refund_type = False
|
153
|
+
|
154
|
+
def _compute_sii_description(self):
|
155
|
+
self.sii_description = "/"
|
156
|
+
|
157
|
+
def _compute_sii_registration_key_domain(self):
|
158
|
+
for document in self:
|
159
|
+
mapping_key = document._get_mapping_key()
|
160
|
+
if mapping_key in {"out_invoice", "out_refund"}:
|
161
|
+
document.sii_registration_key_domain = "sale"
|
162
|
+
elif mapping_key in {"in_invoice", "in_refund"}:
|
163
|
+
document.sii_registration_key_domain = "purchase"
|
164
|
+
else:
|
165
|
+
document.sii_registration_key_domain = False
|
166
|
+
|
167
|
+
@api.depends("fiscal_position_id")
|
168
|
+
def _compute_sii_registration_key(self):
|
169
|
+
for document in self:
|
170
|
+
mapping_key = document._get_mapping_key()
|
171
|
+
if document.fiscal_position_id:
|
172
|
+
if "out" in mapping_key:
|
173
|
+
key = document.fiscal_position_id.sii_registration_key_sale
|
174
|
+
else:
|
175
|
+
key = document.fiscal_position_id.sii_registration_key_purchase
|
176
|
+
# Only assign sii_registration_key if it's set in the fiscal position
|
177
|
+
if key:
|
178
|
+
document.sii_registration_key = key
|
179
|
+
else:
|
180
|
+
domain = [
|
181
|
+
("code", "=", "01"),
|
182
|
+
(
|
183
|
+
"type",
|
184
|
+
"=",
|
185
|
+
"sale" if mapping_key.startswith("out_") else "purchase",
|
186
|
+
),
|
187
|
+
]
|
188
|
+
sii_key_obj = self.env["aeat.sii.mapping.registration.keys"]
|
189
|
+
document.sii_registration_key = sii_key_obj.search(domain, limit=1)
|
190
|
+
|
191
|
+
@api.depends("sii_registration_key")
|
192
|
+
def _compute_sii_registration_key_code(self):
|
193
|
+
"""
|
194
|
+
Para evitar tiempos de instalación largos en BBDD grandes, es necesario que
|
195
|
+
sólo dependa de sii_registration_key, ya que en caso de añadirlo odoo buscará
|
196
|
+
todos los movimientos y cuando escribamos el key, aunque sea un campo no almacenado
|
197
|
+
A partir de v16.0 este cambio ya no es necesario, ya que el sistema ya revisa que el
|
198
|
+
campo sea almacenado o que este visualizandose (en caché)
|
199
|
+
"""
|
200
|
+
for record in self:
|
201
|
+
record.sii_registration_key_code = record.sii_registration_key.code
|
202
|
+
|
203
|
+
def _compute_sii_enabled(self):
|
204
|
+
raise NotImplementedError
|
205
|
+
|
206
|
+
def _compute_macrodata(self):
|
207
|
+
for document in self:
|
208
|
+
document.sii_macrodata = (
|
209
|
+
float_compare(
|
210
|
+
abs(document._get_document_amount_total()),
|
211
|
+
SII_MACRODATA_LIMIT,
|
212
|
+
precision_digits=2,
|
213
|
+
)
|
214
|
+
>= 0
|
215
|
+
)
|
216
|
+
|
217
|
+
def _sii_get_partner(self):
|
218
|
+
raise NotImplementedError
|
219
|
+
|
220
|
+
def _get_sii_country_code(self):
|
221
|
+
self.ensure_one()
|
222
|
+
return self._sii_get_partner()._parse_aeat_vat_info()[0]
|
223
|
+
|
224
|
+
def _filter_sii_unlink_not_possible(self):
|
225
|
+
"""Filter records that we do not allow to be deleted, all those
|
226
|
+
that are not in not_sent sii status."""
|
227
|
+
return self.filtered(lambda rec: rec.sii_state != "not_sent")
|
228
|
+
|
229
|
+
@api.ondelete(at_uninstall=False)
|
230
|
+
def _unlink_except_sii(self):
|
231
|
+
"""Do not allow the deletion of records already sent to the SII."""
|
232
|
+
if self._filter_sii_unlink_not_possible():
|
233
|
+
raise exceptions.UserError(
|
234
|
+
_("You cannot delete an invoice already registered at the SII.")
|
235
|
+
)
|
236
|
+
|
237
|
+
@api.model
|
238
|
+
def _get_sii_taxes_map(self, codes, date):
|
239
|
+
"""Return the codes that correspond to that sii map line codes.
|
240
|
+
|
241
|
+
:param codes: List of code strings to get the mapping.
|
242
|
+
:param date: Date to map
|
243
|
+
:return: Recordset with the corresponding codes
|
244
|
+
"""
|
245
|
+
map_obj = self.env["aeat.sii.map"].sudo()
|
246
|
+
sii_map = map_obj.search(
|
247
|
+
[
|
248
|
+
"|",
|
249
|
+
("date_from", "<=", date),
|
250
|
+
("date_from", "=", False),
|
251
|
+
"|",
|
252
|
+
("date_to", ">=", date),
|
253
|
+
("date_to", "=", False),
|
254
|
+
],
|
255
|
+
limit=1,
|
256
|
+
)
|
257
|
+
tax_templates = sii_map.map_lines.filtered(lambda x: x.code in codes).taxes
|
258
|
+
return self.company_id.get_taxes_from_templates(tax_templates)
|
259
|
+
|
260
|
+
def _change_date_format(self, date):
|
261
|
+
datetimeobject = fields.Date.to_date(date)
|
262
|
+
new_date = datetimeobject.strftime(SII_DATE_FORMAT)
|
263
|
+
return new_date
|
264
|
+
|
265
|
+
def _get_sii_header(self, tipo_comunicacion=False, cancellation=False):
|
266
|
+
"""Builds SII send header
|
267
|
+
|
268
|
+
:param tipo_comunicacion String 'A0': new reg, 'A1': modification
|
269
|
+
:param cancellation Bool True when the communitacion es for document
|
270
|
+
cancellation
|
271
|
+
:return Dict with header data depending on cancellation
|
272
|
+
"""
|
273
|
+
self.ensure_one()
|
274
|
+
if not self.company_id.vat:
|
275
|
+
raise UserError(
|
276
|
+
_("No VAT configured for the company '{}'").format(self.company_id.name)
|
277
|
+
)
|
278
|
+
header = {
|
279
|
+
"IDVersionSii": SII_VERSION,
|
280
|
+
"Titular": {
|
281
|
+
"NombreRazon": self.company_id.name[0:120],
|
282
|
+
"NIF": self.company_id.partner_id._parse_aeat_vat_info()[2],
|
283
|
+
},
|
284
|
+
}
|
285
|
+
if not cancellation:
|
286
|
+
header.update({"TipoComunicacion": tipo_comunicacion})
|
287
|
+
return header
|
288
|
+
|
289
|
+
def _get_sii_jobs_field_name(self):
|
290
|
+
raise NotImplementedError()
|
291
|
+
|
292
|
+
def _cancel_sii_jobs(self):
|
293
|
+
for queue in self.sudo().mapped(self._get_sii_jobs_field_name()):
|
294
|
+
if queue.state == "started":
|
295
|
+
return False
|
296
|
+
elif queue.state in ("pending", "enqueued", "failed"):
|
297
|
+
queue.unlink()
|
298
|
+
return True
|
299
|
+
|
300
|
+
def _get_valid_document_states(self):
|
301
|
+
raise NotImplementedError()
|
302
|
+
|
303
|
+
def send_sii(self):
|
304
|
+
documents = self.filtered(
|
305
|
+
lambda document: (
|
306
|
+
document.sii_enabled
|
307
|
+
and document.state in self._get_valid_document_states()
|
308
|
+
and document.sii_state not in ["sent", "cancelled"]
|
309
|
+
)
|
310
|
+
)
|
311
|
+
if not documents._cancel_sii_jobs():
|
312
|
+
raise UserError(
|
313
|
+
_(
|
314
|
+
"You can not communicate this document at this moment "
|
315
|
+
"because there is a job running!"
|
316
|
+
)
|
317
|
+
)
|
318
|
+
documents._process_sii_send()
|
319
|
+
|
320
|
+
def _process_sii_send(self):
|
321
|
+
"""Process document sending to the SII. Adds general checks from
|
322
|
+
configuration parameters and document availability for SII. If the
|
323
|
+
document is to be sent the decides the send method: direct send or
|
324
|
+
via connector depending on 'Use connector' configuration"""
|
325
|
+
queue_obj = self.env["queue.job"].sudo()
|
326
|
+
for record in self:
|
327
|
+
company = record.company_id
|
328
|
+
if not company.use_connector:
|
329
|
+
record.confirm_one_document()
|
330
|
+
else:
|
331
|
+
eta = company._get_sii_eta()
|
332
|
+
new_delay = (
|
333
|
+
record.sudo()
|
334
|
+
.with_context(company_id=company.id)
|
335
|
+
.with_delay(eta=eta if not record.sii_send_failed else False)
|
336
|
+
.confirm_one_document()
|
337
|
+
)
|
338
|
+
job = queue_obj.search([("uuid", "=", new_delay.uuid)], limit=1)
|
339
|
+
setattr(record.sudo(), self._get_sii_jobs_field_name(), [(4, job.id)])
|
340
|
+
|
341
|
+
def _bind_sii(self, client, port_name, address=None):
|
342
|
+
self.ensure_one()
|
343
|
+
service = client._get_service("siiService")
|
344
|
+
port = client._get_port(service, port_name)
|
345
|
+
address = address or port.binding_options["address"]
|
346
|
+
return client.create_service(port.binding.name, address)
|
347
|
+
|
348
|
+
def _connect_params_sii(self, mapping_key):
|
349
|
+
self.ensure_one()
|
350
|
+
agency = self.company_id.tax_agency_id
|
351
|
+
if not agency:
|
352
|
+
# We use spanish agency by default to keep old behavior with
|
353
|
+
# ir.config parameters. In the future it might be good to reinforce
|
354
|
+
# to explicitly set a tax agency in the company by raising an error
|
355
|
+
# here.
|
356
|
+
agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
|
357
|
+
return agency._connect_params_sii(mapping_key, self.company_id)
|
358
|
+
|
359
|
+
def _connect_sii(self, mapping_key):
|
360
|
+
self.ensure_one()
|
361
|
+
public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates(
|
362
|
+
company=self.company_id
|
363
|
+
)
|
364
|
+
params = self._connect_params_sii(mapping_key)
|
365
|
+
session = Session()
|
366
|
+
session.cert = (public_crt, private_key)
|
367
|
+
transport = Transport(session=session)
|
368
|
+
history = HistoryPlugin()
|
369
|
+
client = Client(wsdl=params["wsdl"], transport=transport, plugins=[history])
|
370
|
+
return self._bind_sii(client, params["port_name"], params["address"])
|
371
|
+
|
372
|
+
def _get_sii_gen_type(self):
|
373
|
+
"""Make a choice for general invoice type
|
374
|
+
|
375
|
+
Returns:
|
376
|
+
int: 1 (National), 2 (Intracom), 3 (Export)
|
377
|
+
"""
|
378
|
+
self.ensure_one()
|
379
|
+
partner_ident = self.fiscal_position_id.sii_partner_identification_type
|
380
|
+
if partner_ident:
|
381
|
+
res = int(partner_ident)
|
382
|
+
elif self.fiscal_position_id.name == "Régimen Intracomunitario":
|
383
|
+
res = 2
|
384
|
+
elif self.fiscal_position_id.name == "Régimen Extracomunitario":
|
385
|
+
res = 3
|
386
|
+
else:
|
387
|
+
res = 1
|
388
|
+
return res
|
389
|
+
|
390
|
+
def _is_sii_simplified_invoice(self):
|
391
|
+
"""Inheritable method to allow control when an
|
392
|
+
invoice are simplified or normal"""
|
393
|
+
partner = self._sii_get_partner()
|
394
|
+
return partner.sii_simplified_invoice
|
395
|
+
|
396
|
+
def _sii_check_exceptions(self):
|
397
|
+
"""Inheritable method for exceptions control when sending SII invoices."""
|
398
|
+
self.ensure_one()
|
399
|
+
gen_type = self._get_sii_gen_type()
|
400
|
+
partner = self._sii_get_partner()
|
401
|
+
country_code = self._get_sii_country_code()
|
402
|
+
is_simplified_invoice = self._is_sii_simplified_invoice()
|
403
|
+
if (
|
404
|
+
(gen_type != 3 or country_code == "ES")
|
405
|
+
and not partner.vat
|
406
|
+
and not is_simplified_invoice
|
407
|
+
):
|
408
|
+
raise UserError(_("The partner has not a VAT configured."))
|
409
|
+
if not self.company_id.chart_template_id:
|
410
|
+
raise UserError(
|
411
|
+
_("You have to select what account chart template use this" " company.")
|
412
|
+
)
|
413
|
+
if not self.company_id.sii_enabled:
|
414
|
+
raise UserError(_("This company doesn't have SII enabled."))
|
415
|
+
if not self.sii_enabled:
|
416
|
+
raise UserError(_("This invoice is not SII enabled."))
|
417
|
+
|
418
|
+
def _get_mapping_key(self):
|
419
|
+
raise NotImplementedError()
|
420
|
+
|
421
|
+
def _get_document_date(self):
|
422
|
+
raise NotImplementedError()
|
423
|
+
|
424
|
+
def _get_document_fiscal_date(self):
|
425
|
+
raise NotImplementedError()
|
426
|
+
|
427
|
+
def _get_document_fiscal_year(self):
|
428
|
+
return fields.Date.to_date(self._get_document_fiscal_date()).year
|
429
|
+
|
430
|
+
def _get_document_period(self):
|
431
|
+
return "%02d" % fields.Date.to_date(self._get_document_fiscal_date()).month
|
432
|
+
|
433
|
+
def _get_document_serial_number(self):
|
434
|
+
raise NotImplementedError()
|
435
|
+
|
436
|
+
def _get_document_product_exempt(self, applied_taxes):
|
437
|
+
raise NotImplementedError()
|
438
|
+
|
439
|
+
def _get_sii_exempt_cause(self, applied_taxes):
|
440
|
+
"""Código de la causa de exención según 3.6 y 3.7 de la FAQ del SII.
|
441
|
+
|
442
|
+
:param applied_taxes: Taxes that are exempt for filtering the lines.
|
443
|
+
"""
|
444
|
+
self.ensure_one()
|
445
|
+
gen_type = self._get_sii_gen_type()
|
446
|
+
if gen_type == 2:
|
447
|
+
return "E5"
|
448
|
+
else:
|
449
|
+
exempt_cause = False
|
450
|
+
product_exempt_causes = self._get_document_product_exempt(applied_taxes)
|
451
|
+
if len(product_exempt_causes) > 1:
|
452
|
+
raise UserError(
|
453
|
+
_("Currently there's no support for multiple exempt causes.")
|
454
|
+
)
|
455
|
+
if product_exempt_causes:
|
456
|
+
exempt_cause = product_exempt_causes.pop()
|
457
|
+
elif (
|
458
|
+
self.fiscal_position_id.sii_exempt_cause
|
459
|
+
and self.fiscal_position_id.sii_exempt_cause != "none"
|
460
|
+
):
|
461
|
+
exempt_cause = self.fiscal_position_id.sii_exempt_cause
|
462
|
+
if gen_type == 3 and exempt_cause not in ["E2", "E3"]:
|
463
|
+
exempt_cause = "E2"
|
464
|
+
return exempt_cause
|
465
|
+
|
466
|
+
def _get_tax_info(self):
|
467
|
+
raise NotImplementedError()
|
468
|
+
|
469
|
+
def _get_sii_tax_req(self, tax):
|
470
|
+
"""Get the associated req tax for the specified tax.
|
471
|
+
|
472
|
+
:param self: Single invoice record.
|
473
|
+
:param tax: Initial tax for searching for the RE linked tax.
|
474
|
+
:return: REQ tax (or empty recordset) linked to the provided tax.
|
475
|
+
"""
|
476
|
+
raise NotImplementedError()
|
477
|
+
|
478
|
+
@api.model
|
479
|
+
def _get_sii_tax_dict(self, tax_line, tax_lines):
|
480
|
+
"""Get the SII tax dictionary for the passed tax line.
|
481
|
+
|
482
|
+
:param self: Single invoice record.
|
483
|
+
:param tax_line: Tax line that is being analyzed.
|
484
|
+
:param tax_lines: Dictionary of processed invoice taxes for further operations
|
485
|
+
(like REQ).
|
486
|
+
:return: A dictionary with the corresponding SII tax values.
|
487
|
+
"""
|
488
|
+
tax = tax_line["tax"]
|
489
|
+
tax_base_amount = tax_line["base"]
|
490
|
+
if tax.amount_type == "group":
|
491
|
+
tax_type = abs(tax.children_tax_ids.filtered("amount")[:1].amount)
|
492
|
+
else:
|
493
|
+
tax_type = abs(tax.amount)
|
494
|
+
tax_dict = {"TipoImpositivo": str(tax_type), "BaseImponible": tax_base_amount}
|
495
|
+
if self._get_mapping_key() in ["out_invoice", "out_refund"]:
|
496
|
+
key = "CuotaRepercutida"
|
497
|
+
else:
|
498
|
+
key = "CuotaSoportada"
|
499
|
+
tax_dict[key] = tax_line["amount"]
|
500
|
+
# Recargo de equivalencia
|
501
|
+
req_tax = self._get_sii_tax_req(tax)
|
502
|
+
if req_tax:
|
503
|
+
tax_dict["TipoRecargoEquivalencia"] = req_tax.amount
|
504
|
+
tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"]
|
505
|
+
return tax_dict
|
506
|
+
|
507
|
+
def _get_no_taxable_cause(self):
|
508
|
+
self.ensure_one()
|
509
|
+
return (
|
510
|
+
self.fiscal_position_id.sii_no_taxable_cause
|
511
|
+
or "ImporteTAIReglasLocalizacion"
|
512
|
+
)
|
513
|
+
|
514
|
+
def _is_sii_type_breakdown_required(self, taxes_dict):
|
515
|
+
"""Calculates if the block 'DesgloseTipoOperacion' is required for
|
516
|
+
the invoice communication."""
|
517
|
+
self.ensure_one()
|
518
|
+
if "DesgloseFactura" not in taxes_dict:
|
519
|
+
return False
|
520
|
+
country_code = self._get_sii_country_code()
|
521
|
+
sii_gen_type = self._get_sii_gen_type()
|
522
|
+
if "DesgloseTipoOperacion" in taxes_dict:
|
523
|
+
# DesgloseTipoOperacion and DesgloseFactura are Exclusive
|
524
|
+
return True
|
525
|
+
elif sii_gen_type in (2, 3):
|
526
|
+
# DesgloseTipoOperacion required for Intracommunity and
|
527
|
+
# Export operations
|
528
|
+
return True
|
529
|
+
elif sii_gen_type == 1 and country_code != "ES":
|
530
|
+
# DesgloseTipoOperacion required for national operations
|
531
|
+
# with 'IDOtro' in the SII identifier block
|
532
|
+
return True
|
533
|
+
elif sii_gen_type == 1 and (self._sii_get_partner().vat or "").startswith(
|
534
|
+
"ESN"
|
535
|
+
):
|
536
|
+
# DesgloseTipoOperacion required if customer's country is Spain and
|
537
|
+
# has a NIF which starts with 'N'
|
538
|
+
return True
|
539
|
+
return False
|
540
|
+
|
541
|
+
def _get_sii_out_taxes(self): # noqa: C901
|
542
|
+
"""Get the taxes for sales documents.
|
543
|
+
|
544
|
+
:param self: Single document record.
|
545
|
+
"""
|
546
|
+
self.ensure_one()
|
547
|
+
taxes_dict = {}
|
548
|
+
date = self._get_document_fiscal_date()
|
549
|
+
taxes_sfesb = self._get_sii_taxes_map(["SFESB"], date)
|
550
|
+
taxes_sfesbe = self._get_sii_taxes_map(["SFESBE"], date)
|
551
|
+
taxes_sfesisp = self._get_sii_taxes_map(["SFESISP"], date)
|
552
|
+
# taxes_sfesisps = self._get_taxes_map(['SFESISPS'])
|
553
|
+
taxes_sfens = self._get_sii_taxes_map(["SFENS"], date)
|
554
|
+
taxes_sfess = self._get_sii_taxes_map(["SFESS"], date)
|
555
|
+
taxes_sfesse = self._get_sii_taxes_map(["SFESSE"], date)
|
556
|
+
taxes_sfesns = self._get_sii_taxes_map(["SFESNS"], date)
|
557
|
+
taxes_not_in_total = self._get_sii_taxes_map(["NotIncludedInTotal"], date)
|
558
|
+
taxes_not_in_total_neg = self._get_sii_taxes_map(
|
559
|
+
["NotIncludedInTotalNegative"], date
|
560
|
+
)
|
561
|
+
base_not_in_total = self._get_sii_taxes_map(["BaseNotIncludedInTotal"], date)
|
562
|
+
not_in_amount_total = 0
|
563
|
+
exempt_cause = self._get_sii_exempt_cause(taxes_sfesbe + taxes_sfesse)
|
564
|
+
tax_lines = self._get_tax_info()
|
565
|
+
for tax_line in tax_lines.values():
|
566
|
+
tax = tax_line["tax"]
|
567
|
+
breakdown_taxes = taxes_sfesb + taxes_sfesisp + taxes_sfens + taxes_sfesbe
|
568
|
+
if tax in taxes_not_in_total:
|
569
|
+
not_in_amount_total += tax_line["amount"]
|
570
|
+
elif tax in taxes_not_in_total_neg:
|
571
|
+
not_in_amount_total -= tax_line["amount"]
|
572
|
+
elif tax in base_not_in_total:
|
573
|
+
not_in_amount_total += tax_line["base"]
|
574
|
+
if tax in breakdown_taxes:
|
575
|
+
tax_breakdown = taxes_dict.setdefault("DesgloseFactura", {})
|
576
|
+
if tax in (taxes_sfesb + taxes_sfesbe + taxes_sfesisp):
|
577
|
+
sub_dict = tax_breakdown.setdefault("Sujeta", {})
|
578
|
+
# TODO l10n_es no tiene impuesto exento de bienes
|
579
|
+
# corrientes nacionales
|
580
|
+
if tax in taxes_sfesbe:
|
581
|
+
exempt_dict = sub_dict.setdefault(
|
582
|
+
"Exenta",
|
583
|
+
{"DetalleExenta": [{"BaseImponible": 0}]},
|
584
|
+
)
|
585
|
+
det_dict = exempt_dict["DetalleExenta"][0]
|
586
|
+
if exempt_cause:
|
587
|
+
det_dict["CausaExencion"] = exempt_cause
|
588
|
+
det_dict["BaseImponible"] += tax_line["base"]
|
589
|
+
else:
|
590
|
+
sub_dict.setdefault(
|
591
|
+
"NoExenta",
|
592
|
+
{
|
593
|
+
"TipoNoExenta": ("S2" if tax in taxes_sfesisp else "S1"),
|
594
|
+
"DesgloseIVA": {"DetalleIVA": []},
|
595
|
+
},
|
596
|
+
)
|
597
|
+
not_ex_type = sub_dict["NoExenta"]["TipoNoExenta"]
|
598
|
+
if tax in taxes_sfesisp:
|
599
|
+
is_s3 = not_ex_type == "S1"
|
600
|
+
else:
|
601
|
+
is_s3 = not_ex_type == "S2"
|
602
|
+
if is_s3:
|
603
|
+
sub_dict["NoExenta"]["TipoNoExenta"] = "S3"
|
604
|
+
sub_dict["NoExenta"]["DesgloseIVA"]["DetalleIVA"].append(
|
605
|
+
self._get_sii_tax_dict(tax_line, tax_lines),
|
606
|
+
)
|
607
|
+
# No sujetas
|
608
|
+
if tax in taxes_sfens:
|
609
|
+
# ImporteTAIReglasLocalizacion or ImportePorArticulos7_14_Otros
|
610
|
+
default_no_taxable_cause = self._get_no_taxable_cause()
|
611
|
+
nsub_dict = tax_breakdown.setdefault(
|
612
|
+
"NoSujeta",
|
613
|
+
{default_no_taxable_cause: 0},
|
614
|
+
)
|
615
|
+
nsub_dict[default_no_taxable_cause] += tax_line["base"]
|
616
|
+
if tax in (taxes_sfess + taxes_sfesse + taxes_sfesns):
|
617
|
+
type_breakdown = taxes_dict.setdefault(
|
618
|
+
"DesgloseTipoOperacion",
|
619
|
+
{"PrestacionServicios": {}},
|
620
|
+
)
|
621
|
+
if tax in (taxes_sfesse + taxes_sfess):
|
622
|
+
type_breakdown["PrestacionServicios"].setdefault("Sujeta", {})
|
623
|
+
service_dict = type_breakdown["PrestacionServicios"]
|
624
|
+
if tax in taxes_sfesse:
|
625
|
+
exempt_dict = service_dict["Sujeta"].setdefault(
|
626
|
+
"Exenta",
|
627
|
+
{"DetalleExenta": [{"BaseImponible": 0}]},
|
628
|
+
)
|
629
|
+
det_dict = exempt_dict["DetalleExenta"][0]
|
630
|
+
if exempt_cause:
|
631
|
+
det_dict["CausaExencion"] = exempt_cause
|
632
|
+
det_dict["BaseImponible"] += tax_line["base"]
|
633
|
+
if tax in taxes_sfess:
|
634
|
+
# TODO l10n_es_ no tiene impuesto ISP de servicios
|
635
|
+
# if tax in taxes_sfesisps:
|
636
|
+
# TipoNoExenta = 'S2'
|
637
|
+
# else:
|
638
|
+
service_dict["Sujeta"].setdefault(
|
639
|
+
"NoExenta",
|
640
|
+
{"TipoNoExenta": "S1", "DesgloseIVA": {"DetalleIVA": []}},
|
641
|
+
)
|
642
|
+
sub = type_breakdown["PrestacionServicios"]["Sujeta"]["NoExenta"][
|
643
|
+
"DesgloseIVA"
|
644
|
+
]["DetalleIVA"]
|
645
|
+
sub.append(self._get_sii_tax_dict(tax_line, tax_lines))
|
646
|
+
if tax in taxes_sfesns:
|
647
|
+
nsub_dict = service_dict.setdefault(
|
648
|
+
"NoSujeta",
|
649
|
+
{"ImporteTAIReglasLocalizacion": 0},
|
650
|
+
)
|
651
|
+
nsub_dict["ImporteTAIReglasLocalizacion"] += tax_line["base"]
|
652
|
+
# Ajustes finales breakdown
|
653
|
+
# - DesgloseFactura y DesgloseTipoOperacion son excluyentes
|
654
|
+
# - Ciertos condicionantes obligan DesgloseTipoOperacion
|
655
|
+
if self._is_sii_type_breakdown_required(taxes_dict):
|
656
|
+
taxes_dict.setdefault("DesgloseTipoOperacion", {})
|
657
|
+
taxes_dict["DesgloseTipoOperacion"]["Entrega"] = taxes_dict[
|
658
|
+
"DesgloseFactura"
|
659
|
+
]
|
660
|
+
del taxes_dict["DesgloseFactura"]
|
661
|
+
return taxes_dict, not_in_amount_total
|
662
|
+
|
663
|
+
def _get_document_amount_total(self):
|
664
|
+
raise NotImplementedError()
|
665
|
+
|
666
|
+
def _get_sii_invoice_type(self):
|
667
|
+
raise NotImplementedError()
|
668
|
+
|
669
|
+
def _get_sii_identifier(self):
|
670
|
+
"""Get the SII structure for a partner identifier depending on the
|
671
|
+
conditions of the invoice.
|
672
|
+
"""
|
673
|
+
self.ensure_one()
|
674
|
+
gen_type = self._get_sii_gen_type()
|
675
|
+
(
|
676
|
+
country_code,
|
677
|
+
identifier_type,
|
678
|
+
identifier,
|
679
|
+
) = self._sii_get_partner()._parse_aeat_vat_info()
|
680
|
+
# Limpiar alfanum
|
681
|
+
if identifier:
|
682
|
+
identifier = "".join(e for e in identifier if e.isalnum()).upper()
|
683
|
+
else:
|
684
|
+
identifier = "NO_DISPONIBLE"
|
685
|
+
identifier_type = "06"
|
686
|
+
if gen_type == 1:
|
687
|
+
if "1117" in (self.sii_send_error or ""):
|
688
|
+
return {
|
689
|
+
"IDOtro": {
|
690
|
+
"CodigoPais": country_code,
|
691
|
+
"IDType": "07",
|
692
|
+
"ID": identifier,
|
693
|
+
}
|
694
|
+
}
|
695
|
+
else:
|
696
|
+
if identifier_type == "":
|
697
|
+
return {"NIF": identifier}
|
698
|
+
return {
|
699
|
+
"IDOtro": {
|
700
|
+
"CodigoPais": country_code,
|
701
|
+
"IDType": identifier_type,
|
702
|
+
"ID": country_code + identifier
|
703
|
+
if self._sii_get_partner()._map_aeat_country_code(country_code)
|
704
|
+
in self._sii_get_partner()._get_aeat_europe_codes()
|
705
|
+
else identifier,
|
706
|
+
},
|
707
|
+
}
|
708
|
+
elif gen_type == 2:
|
709
|
+
return {"IDOtro": {"IDType": "02", "ID": country_code + identifier}}
|
710
|
+
elif gen_type == 3 and identifier_type:
|
711
|
+
# Si usamos identificador tipo 02 en exportaciones, el envío falla con:
|
712
|
+
# {'CodigoErrorRegistro': 1104,
|
713
|
+
# 'DescripcionErrorRegistro': 'Valor del campo ID incorrecto'}
|
714
|
+
if identifier_type == "02":
|
715
|
+
identifier_type = "06"
|
716
|
+
return {
|
717
|
+
"IDOtro": {
|
718
|
+
"CodigoPais": country_code,
|
719
|
+
"IDType": identifier_type,
|
720
|
+
"ID": identifier,
|
721
|
+
},
|
722
|
+
}
|
723
|
+
elif gen_type == 3:
|
724
|
+
return {"NIF": identifier}
|
725
|
+
|
726
|
+
def _get_sii_invoice_dict_out(self, cancel=False):
|
727
|
+
"""Build dict with data to send to AEAT WS for document types:
|
728
|
+
out_invoice and out_refund.
|
729
|
+
|
730
|
+
:param cancel: It indicates if the dictionary is for sending a
|
731
|
+
cancellation of the document.
|
732
|
+
:return: documents (dict) : Dict XML with data for this document.
|
733
|
+
"""
|
734
|
+
self.ensure_one()
|
735
|
+
document_date = self._change_date_format(self._get_document_date())
|
736
|
+
partner = self._sii_get_partner()
|
737
|
+
company = self.company_id
|
738
|
+
fiscal_year = self._get_document_fiscal_year()
|
739
|
+
period = self._get_document_period()
|
740
|
+
is_simplified_invoice = self._is_sii_simplified_invoice()
|
741
|
+
serial_number = self._get_document_serial_number()
|
742
|
+
inv_dict = {
|
743
|
+
"IDFactura": {
|
744
|
+
"IDEmisorFactura": {
|
745
|
+
"NIF": company.partner_id._parse_aeat_vat_info()[2]
|
746
|
+
},
|
747
|
+
# On cancelled invoices, number is not filled
|
748
|
+
"NumSerieFacturaEmisor": serial_number,
|
749
|
+
"FechaExpedicionFacturaEmisor": document_date,
|
750
|
+
},
|
751
|
+
"PeriodoLiquidacion": {
|
752
|
+
"Ejercicio": fiscal_year,
|
753
|
+
"Periodo": period,
|
754
|
+
},
|
755
|
+
}
|
756
|
+
if not cancel:
|
757
|
+
tipo_desglose, not_in_amount_total = self._get_sii_out_taxes()
|
758
|
+
amount_total = self._get_document_amount_total() - not_in_amount_total
|
759
|
+
inv_dict["FacturaExpedida"] = {
|
760
|
+
"TipoFactura": self._get_sii_invoice_type(),
|
761
|
+
"ClaveRegimenEspecialOTrascendencia": (self.sii_registration_key.code),
|
762
|
+
"DescripcionOperacion": self.sii_description,
|
763
|
+
"TipoDesglose": tipo_desglose,
|
764
|
+
"ImporteTotal": amount_total,
|
765
|
+
}
|
766
|
+
if self.sii_macrodata:
|
767
|
+
inv_dict["FacturaExpedida"].update(Macrodato="S")
|
768
|
+
exp_dict = inv_dict["FacturaExpedida"]
|
769
|
+
if not is_simplified_invoice:
|
770
|
+
# Simplified invoices don't have counterpart
|
771
|
+
exp_dict["Contraparte"] = {
|
772
|
+
"NombreRazon": partner.name[0:120],
|
773
|
+
}
|
774
|
+
# Uso condicional de IDOtro/NIF
|
775
|
+
exp_dict["Contraparte"].update(self._get_sii_identifier())
|
776
|
+
return inv_dict
|
777
|
+
|
778
|
+
def _get_sii_invoice_dict_in(self, cancel=False):
|
779
|
+
"""Build dict with data to send to AEAT WS for invoice types:
|
780
|
+
in_invoice and in_refund.
|
781
|
+
|
782
|
+
:param cancel: It indicates if the dictionary if for sending a
|
783
|
+
cancellation of the invoice.
|
784
|
+
:return: invoices (dict) : Dict XML with data for this invoice.
|
785
|
+
"""
|
786
|
+
raise NotImplementedError()
|
787
|
+
|
788
|
+
def _get_sii_invoice_dict(self):
|
789
|
+
self.ensure_one()
|
790
|
+
self._sii_check_exceptions()
|
791
|
+
inv_dict = {}
|
792
|
+
mapping_key = self._get_mapping_key()
|
793
|
+
if mapping_key in ["out_invoice", "out_refund"]:
|
794
|
+
inv_dict = self._get_sii_invoice_dict_out()
|
795
|
+
elif mapping_key in ["in_invoice", "in_refund"]:
|
796
|
+
inv_dict = self._get_sii_invoice_dict_in()
|
797
|
+
round_by_keys(
|
798
|
+
inv_dict,
|
799
|
+
[
|
800
|
+
"BaseImponible",
|
801
|
+
"CuotaRepercutida",
|
802
|
+
"CuotaSoportada",
|
803
|
+
"TipoRecargoEquivalencia",
|
804
|
+
"CuotaRecargoEquivalencia",
|
805
|
+
"ImportePorArticulos7_14_Otros",
|
806
|
+
"ImporteTAIReglasLocalizacion",
|
807
|
+
"ImporteTotal",
|
808
|
+
"BaseRectificada",
|
809
|
+
"CuotaRectificada",
|
810
|
+
"CuotaDeducible",
|
811
|
+
"ImporteCompensacionREAGYP",
|
812
|
+
],
|
813
|
+
)
|
814
|
+
return inv_dict
|
815
|
+
|
816
|
+
def _get_account_registration_date(self):
|
817
|
+
"""Hook method to allow the setting of the account registration date
|
818
|
+
of each supplier invoice. The SII recommends to set the send date as
|
819
|
+
the default value (point 9.3 of the document
|
820
|
+
SII_Descripcion_ServicioWeb_v0.7.pdf), so by default we return
|
821
|
+
the current date or, if exists, the stored
|
822
|
+
sii_account_registration_date
|
823
|
+
:return String date in the format %Y-%m-%d"""
|
824
|
+
self.ensure_one()
|
825
|
+
return self.sii_account_registration_date or fields.Date.today()
|
826
|
+
|
827
|
+
def _send_document_to_sii(self):
|
828
|
+
for document in self.filtered(
|
829
|
+
lambda i: i.state in self._get_valid_document_states()
|
830
|
+
):
|
831
|
+
if document.sii_state == "not_sent":
|
832
|
+
tipo_comunicacion = "A0"
|
833
|
+
else:
|
834
|
+
tipo_comunicacion = "A1"
|
835
|
+
header = document._get_sii_header(tipo_comunicacion)
|
836
|
+
doc_vals = {
|
837
|
+
"sii_header_sent": json.dumps(header, indent=4),
|
838
|
+
}
|
839
|
+
# add this extra try except in case _get_sii_invoice_dict fails
|
840
|
+
# if not, get the value doc_dict for the next try and except below
|
841
|
+
try:
|
842
|
+
inv_dict = document._get_sii_invoice_dict()
|
843
|
+
except Exception as fault:
|
844
|
+
raise ValidationError(fault) from fault
|
845
|
+
try:
|
846
|
+
mapping_key = document._get_mapping_key()
|
847
|
+
serv = document._connect_sii(mapping_key)
|
848
|
+
doc_vals["sii_content_sent"] = json.dumps(inv_dict, indent=4)
|
849
|
+
if mapping_key in ["out_invoice", "out_refund"]:
|
850
|
+
res = serv.SuministroLRFacturasEmitidas(header, inv_dict)
|
851
|
+
elif mapping_key in ["in_invoice", "in_refund"]:
|
852
|
+
res = serv.SuministroLRFacturasRecibidas(header, inv_dict)
|
853
|
+
# TODO Facturas intracomunitarias 66 RIVA
|
854
|
+
# elif invoice.fiscal_position_id.id == self.env.ref(
|
855
|
+
# 'account.fp_intra').id:
|
856
|
+
# res = serv.SuministroLRDetOperacionIntracomunitaria(
|
857
|
+
# header, invoices)
|
858
|
+
res_line = res["RespuestaLinea"][0]
|
859
|
+
if res["EstadoEnvio"] == "Correcto":
|
860
|
+
doc_vals.update(
|
861
|
+
{
|
862
|
+
"sii_state": "sent",
|
863
|
+
"sii_csv": res["CSV"],
|
864
|
+
"sii_send_failed": False,
|
865
|
+
}
|
866
|
+
)
|
867
|
+
elif (
|
868
|
+
res["EstadoEnvio"] == "ParcialmenteCorrecto"
|
869
|
+
and res_line["EstadoRegistro"] == "AceptadoConErrores"
|
870
|
+
):
|
871
|
+
doc_vals.update(
|
872
|
+
{
|
873
|
+
"sii_state": "sent_w_errors",
|
874
|
+
"sii_csv": res["CSV"],
|
875
|
+
"sii_send_failed": True,
|
876
|
+
}
|
877
|
+
)
|
878
|
+
else:
|
879
|
+
doc_vals["sii_send_failed"] = True
|
880
|
+
if (
|
881
|
+
"sii_state" in doc_vals
|
882
|
+
and not document.sii_account_registration_date
|
883
|
+
and mapping_key[:2] == "in"
|
884
|
+
):
|
885
|
+
doc_vals[
|
886
|
+
"sii_account_registration_date"
|
887
|
+
] = self._get_account_registration_date()
|
888
|
+
doc_vals["sii_return"] = res
|
889
|
+
send_error = False
|
890
|
+
if res_line["CodigoErrorRegistro"]:
|
891
|
+
send_error = "{} | {}".format(
|
892
|
+
str(res_line["CodigoErrorRegistro"]),
|
893
|
+
str(res_line["DescripcionErrorRegistro"])[:60],
|
894
|
+
)
|
895
|
+
doc_vals["sii_send_error"] = send_error
|
896
|
+
document.write(doc_vals)
|
897
|
+
except Exception as fault:
|
898
|
+
new_cr = Registry(self.env.cr.dbname).cursor()
|
899
|
+
env = api.Environment(new_cr, self.env.uid, self.env.context)
|
900
|
+
document = env[document._name].browse(document.id)
|
901
|
+
doc_vals.update(
|
902
|
+
{
|
903
|
+
"sii_send_failed": True,
|
904
|
+
"sii_send_error": repr(fault)[:60],
|
905
|
+
"sii_return": repr(fault),
|
906
|
+
"sii_content_sent": json.dumps(inv_dict, indent=4),
|
907
|
+
}
|
908
|
+
)
|
909
|
+
document.write(doc_vals)
|
910
|
+
new_cr.commit()
|
911
|
+
new_cr.close()
|
912
|
+
raise ValidationError(fault) from fault
|
913
|
+
|
914
|
+
def confirm_one_document(self):
|
915
|
+
self.sudo()._send_document_to_sii()
|