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.
Files changed (34) hide show
  1. odoo/addons/l10n_es_aeat_sii_oca/README.rst +1 -1
  2. odoo/addons/l10n_es_aeat_sii_oca/__manifest__.py +1 -1
  3. odoo/addons/l10n_es_aeat_sii_oca/data/aeat_sii_queue_job.xml +3 -3
  4. odoo/addons/l10n_es_aeat_sii_oca/i18n/bg.po +61 -18
  5. odoo/addons/l10n_es_aeat_sii_oca/i18n/ca.po +99 -28
  6. odoo/addons/l10n_es_aeat_sii_oca/i18n/cs.po +61 -15
  7. odoo/addons/l10n_es_aeat_sii_oca/i18n/de.po +61 -15
  8. odoo/addons/l10n_es_aeat_sii_oca/i18n/es.po +122 -36
  9. odoo/addons/l10n_es_aeat_sii_oca/i18n/es_CO.po +61 -15
  10. odoo/addons/l10n_es_aeat_sii_oca/i18n/es_CR.po +61 -15
  11. odoo/addons/l10n_es_aeat_sii_oca/i18n/eu.po +61 -18
  12. odoo/addons/l10n_es_aeat_sii_oca/i18n/fr.po +61 -18
  13. odoo/addons/l10n_es_aeat_sii_oca/i18n/gl.po +61 -18
  14. odoo/addons/l10n_es_aeat_sii_oca/i18n/hr.po +61 -18
  15. odoo/addons/l10n_es_aeat_sii_oca/i18n/l10n_es_aeat_sii_oca.pot +62 -16
  16. odoo/addons/l10n_es_aeat_sii_oca/i18n/nl.po +61 -15
  17. odoo/addons/l10n_es_aeat_sii_oca/i18n/pl.po +61 -18
  18. odoo/addons/l10n_es_aeat_sii_oca/i18n/pt.po +61 -18
  19. odoo/addons/l10n_es_aeat_sii_oca/i18n/pt_BR.po +61 -18
  20. odoo/addons/l10n_es_aeat_sii_oca/i18n/ru.po +61 -15
  21. odoo/addons/l10n_es_aeat_sii_oca/i18n/sl.po +61 -18
  22. odoo/addons/l10n_es_aeat_sii_oca/i18n/sv.po +61 -15
  23. odoo/addons/l10n_es_aeat_sii_oca/i18n/tr.po +61 -15
  24. odoo/addons/l10n_es_aeat_sii_oca/i18n/vi.po +61 -15
  25. odoo/addons/l10n_es_aeat_sii_oca/migrations/16.0.1.4.0/post-migration.py +18 -0
  26. odoo/addons/l10n_es_aeat_sii_oca/models/__init__.py +1 -0
  27. odoo/addons/l10n_es_aeat_sii_oca/models/account_move.py +126 -693
  28. odoo/addons/l10n_es_aeat_sii_oca/models/sii_mixin.py +915 -0
  29. odoo/addons/l10n_es_aeat_sii_oca/static/description/index.html +1 -1
  30. odoo/addons/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py +38 -43
  31. {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
  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}/RECORD +34 -32
  33. {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
  34. {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
@@ -7,25 +7,19 @@
7
7
  # Copyright 2021 Tecnativa - João Marques
8
8
  # Copyright 2022 ForgeFlow - Lois Rilo
9
9
  # Copyright 2011-2023 Tecnativa - Pedro M. Baeza
10
+ # Copyright 2023 Aures Tic - Almudena de la Puente <almudena@aurestic.es>
11
+ # Copyright 2023 Aures Tic - Jose Zambudio <jose@aurestic.es>
10
12
  # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
11
13
 
12
14
  import json
13
15
  import logging
14
16
 
15
- from requests import Session
16
-
17
17
  from odoo import _, api, exceptions, fields, models
18
18
  from odoo.modules.registry import Registry
19
- from odoo.tools.float_utils import float_compare
20
19
 
20
+ SII_VALID_INVOICE_STATES = ["posted"]
21
21
  _logger = logging.getLogger(__name__)
22
22
 
23
- try:
24
- from zeep import Client
25
- from zeep.plugins import HistoryPlugin
26
- from zeep.transports import Transport
27
- except (ImportError, IOError) as err:
28
- _logger.debug(err)
29
23
 
30
24
  try:
31
25
  from odoo.addons.queue_job.job import job
@@ -38,84 +32,19 @@ except ImportError:
38
32
 
39
33
  job = empty_decorator_factory
40
34
 
41
- SII_STATES = [
42
- ("not_sent", "Not sent"),
43
- ("sent", "Sent"),
44
- ("sent_w_errors", "Accepted with errors"),
45
- ("sent_modified", "Registered in SII but last modifications not sent"),
46
- ("cancelled", "Cancelled"),
47
- ("cancelled_modified", "Cancelled in SII but last modifications not sent"),
48
- ]
49
- SII_VERSION = "1.1"
50
- SII_MACRODATA_LIMIT = 100000000.0
51
- SII_VALID_INVOICE_STATES = ["posted"]
52
-
53
35
 
54
- def round_by_keys(elem, search_keys, prec=2):
55
- """This uses ``round`` method directly as if has been tested that Odoo's
56
- ``float_round`` still returns incorrect amounts for certain values. Try
57
- 3 units x 3,77 €/unit with 10% tax and you will be hit by the error
58
- (on regular x86 architectures)."""
59
- if isinstance(elem, dict):
60
- for key, value in elem.items():
61
- if key in search_keys:
62
- elem[key] = round(elem[key], prec)
63
- else:
64
- round_by_keys(value, search_keys)
65
- elif isinstance(elem, list):
66
- for value in elem:
67
- round_by_keys(value, search_keys)
36
+ class AccountMove(models.Model):
37
+ _name = "account.move"
38
+ _inherit = ["account.move", "sii.mixin"]
68
39
 
40
+ def _get_default_type(self):
41
+ context = self.env.context
42
+ return context.get("move_type", context.get("default_move_type"))
69
43
 
70
- class AccountMove(models.Model):
71
- _inherit = "account.move"
44
+ def _default_sii_refund_type(self):
45
+ inv_type = self._get_default_type()
46
+ return "I" if inv_type in ["out_refund", "in_refund"] else False
72
47
 
73
- sii_description = fields.Text(
74
- string="SII computed description",
75
- compute="_compute_sii_description",
76
- default="/",
77
- store=True,
78
- readonly=False,
79
- copy=False,
80
- )
81
- sii_state = fields.Selection(
82
- selection=SII_STATES,
83
- string="SII send state",
84
- default="not_sent",
85
- readonly=True,
86
- copy=False,
87
- help="Indicates the state of this invoice in relation with the "
88
- "presentation at the SII",
89
- )
90
- sii_csv = fields.Char(string="SII CSV", copy=False, readonly=True)
91
- sii_return = fields.Text(string="SII Return", copy=False, readonly=True)
92
- sii_header_sent = fields.Text(
93
- string="SII last header sent",
94
- copy=False,
95
- readonly=True,
96
- )
97
- sii_content_sent = fields.Text(
98
- string="SII last content sent",
99
- copy=False,
100
- readonly=True,
101
- )
102
- sii_send_error = fields.Text(string="SII Send Error", readonly=True, copy=False)
103
- sii_send_failed = fields.Boolean(
104
- string="SII send failed",
105
- copy=False,
106
- help="Indicates that the last attempt to communicate this invoice to "
107
- "the SII has failed. See SII return for details",
108
- )
109
- sii_refund_type = fields.Selection(
110
- selection=[
111
- # ('S', 'By substitution'), - Removed as not fully supported
112
- ("I", "By differences"),
113
- ],
114
- string="SII Refund Type",
115
- compute="_compute_sii_refund_type",
116
- store=True,
117
- readonly=False,
118
- )
119
48
  sii_refund_specific_invoice_type = fields.Selection(
120
49
  selection=[
121
50
  ("R1", "Error based on law and Art. 80 One and Two LIVA (R1)"),
@@ -127,28 +56,6 @@ class AccountMove(models.Model):
127
56
  " of article 80 of LIVA for notifying to SII with the proper"
128
57
  " invoice type.",
129
58
  )
130
- sii_account_registration_date = fields.Date(
131
- string="SII account registration date",
132
- readonly=True,
133
- copy=False,
134
- help="Indicates the account registration date set at the SII, which "
135
- "must be the date when the invoice is recorded in the system and "
136
- "is independent of the date of the accounting entry of the "
137
- "invoice",
138
- )
139
- sii_registration_key_domain = fields.Char(
140
- compute="_compute_sii_registration_key_domain",
141
- string="SII registration key domain",
142
- )
143
- sii_registration_key = fields.Many2one(
144
- comodel_name="aeat.sii.mapping.registration.keys",
145
- string="SII registration key",
146
- compute="_compute_sii_registration_key",
147
- store=True,
148
- readonly=False,
149
- # required=True, This is not set as required here to avoid the
150
- # set not null constraint warning
151
- )
152
59
  sii_registration_key_additional1 = fields.Many2one(
153
60
  comodel_name="aeat.sii.mapping.registration.keys",
154
61
  string="Additional SII registration key",
@@ -157,12 +64,6 @@ class AccountMove(models.Model):
157
64
  comodel_name="aeat.sii.mapping.registration.keys",
158
65
  string="Additional 2 SII registration key",
159
66
  )
160
- sii_registration_key_code = fields.Char(
161
- related="sii_registration_key.code",
162
- readonly=True,
163
- string="SII Code",
164
- )
165
- sii_enabled = fields.Boolean(string="Enable SII", compute="_compute_sii_enabled")
166
67
  sii_property_location = fields.Selection(
167
68
  string="Real property location",
168
69
  copy=False,
@@ -185,12 +86,6 @@ class AccountMove(models.Model):
185
86
  string="Real property cadastrial code",
186
87
  copy=False,
187
88
  )
188
- sii_macrodata = fields.Boolean(
189
- string="MacroData",
190
- help="Check to confirm that the invoice has an absolute amount "
191
- "greater o equal to 100 000 000,00 euros.",
192
- compute="_compute_macrodata",
193
- )
194
89
  sii_lc_operation = fields.Boolean(
195
90
  string="Customs - Complementary settlement",
196
91
  help="Check this mark if this invoice represents a complementary "
@@ -209,51 +104,23 @@ class AccountMove(models.Model):
209
104
 
210
105
  @api.depends("move_type")
211
106
  def _compute_sii_refund_type(self):
212
- self.sii_refund_type = False
213
107
  for record in self:
214
108
  if "refund" in (record.move_type or ""):
215
109
  record.sii_refund_type = "I"
110
+ else:
111
+ record.sii_refund_type = False
216
112
 
217
113
  @api.depends("move_type")
218
114
  def _compute_sii_registration_key_domain(self):
219
- for record in self:
220
- if record.move_type in {"out_invoice", "out_refund"}:
221
- record.sii_registration_key_domain = "sale"
222
- elif record.move_type in {"in_invoice", "in_refund"}:
223
- record.sii_registration_key_domain = "purchase"
224
- else:
225
- record.sii_registration_key_domain = False
115
+ return super()._compute_sii_registration_key_domain()
226
116
 
227
- @api.depends("fiscal_position_id", "move_type")
117
+ @api.depends("move_type")
228
118
  def _compute_sii_registration_key(self):
229
- for invoice in self:
230
- if invoice.fiscal_position_id:
231
- if "out" in invoice.move_type:
232
- key = invoice.fiscal_position_id.sii_registration_key_sale
233
- else:
234
- key = invoice.fiscal_position_id.sii_registration_key_purchase
235
- # Only assign sii_registration_key if it's set in the fiscal position
236
- if key:
237
- invoice.sii_registration_key = key
238
- else:
239
- domain = [
240
- ("code", "=", "01"),
241
- ("type", "=", "sale" if "out" in invoice.move_type else "purchase"),
242
- ]
243
- sii_key_obj = self.env["aeat.sii.mapping.registration.keys"]
244
- invoice.sii_registration_key = sii_key_obj.search(domain, limit=1)
119
+ return super()._compute_sii_registration_key()
245
120
 
246
121
  @api.depends("amount_total")
247
122
  def _compute_macrodata(self):
248
- for inv in self:
249
- inv.sii_macrodata = (
250
- float_compare(
251
- abs(inv.amount_total_signed),
252
- SII_MACRODATA_LIMIT,
253
- precision_digits=2,
254
- )
255
- >= 0
256
- )
123
+ return super()._compute_macrodata()
257
124
 
258
125
  def _sii_get_partner(self):
259
126
  return self.commercial_partner_id
@@ -292,67 +159,10 @@ class AccountMove(models.Model):
292
159
  self._raise_exception_sii(_("invoice number"))
293
160
  return super().write(vals)
294
161
 
295
- def unlink(self):
296
- """A registered invoice at the SII cannot be deleted"""
297
- for invoice in self.filtered(lambda x: x.is_invoice()):
298
- if invoice.sii_state != "not_sent":
299
- raise exceptions.UserError(
300
- _("You cannot delete an invoice already registered at the " "SII.")
301
- )
302
- return super().unlink()
303
-
304
- def _get_sii_taxes_map(self, codes):
305
- """Return the codes that correspond to that sii map line codes.
306
-
307
- :param self: Single invoice record.
308
- :param codes: List of code strings to get the mapping.
309
- :return: Recordset with the corresponding codes
310
- """
311
- self.ensure_one()
312
- map_obj = self.env["aeat.sii.map"].sudo()
313
- sii_map = map_obj.search(
314
- [
315
- "|",
316
- ("date_from", "<=", self.date),
317
- ("date_from", "=", False),
318
- "|",
319
- ("date_to", ">=", self.date),
320
- ("date_to", "=", False),
321
- ],
322
- limit=1,
323
- )
324
- tax_templates = sii_map.map_lines.filtered(lambda x: x.code in codes).taxes
325
- return self.company_id.get_taxes_from_templates(tax_templates)
326
-
327
- def _change_date_format(self, date):
328
- datetimeobject = fields.Date.to_date(date)
329
- new_date = datetimeobject.strftime("%d-%m-%Y")
330
- return new_date
331
-
332
- def _get_sii_header(self, tipo_comunicacion=False, cancellation=False):
333
- """Builds SII send header
334
-
335
- :param tipo_comunicacion String 'A0': new reg, 'A1': modification
336
- :param cancellation Bool True when the communitacion es for invoice
337
- cancellation
338
- :return Dict with header data depending on cancellation
339
- """
340
- self.ensure_one()
341
- company = self.company_id
342
- if not company.vat:
343
- raise exceptions.UserError(
344
- _("No VAT configured for the company '{}'").format(company.name)
345
- )
346
- header = {
347
- "IDVersionSii": SII_VERSION,
348
- "Titular": {
349
- "NombreRazon": self.company_id.name[0:120],
350
- "NIF": company.partner_id._parse_aeat_vat_info()[2],
351
- },
352
- }
353
- if not cancellation:
354
- header.update({"TipoComunicacion": tipo_comunicacion})
355
- return header
162
+ def _filter_sii_unlink_not_possible(self):
163
+ """Filter records that we can delete to apply only to invoices."""
164
+ res = super()._filter_sii_unlink_not_possible()
165
+ return res.filtered(lambda x: x.is_invoice())
356
166
 
357
167
  def _get_sii_tax_req(self, tax):
358
168
  """Get the associated req tax for the specified tax.
@@ -362,7 +172,7 @@ class AccountMove(models.Model):
362
172
  :return: REQ tax (or empty recordset) linked to the provided tax.
363
173
  """
364
174
  self.ensure_one()
365
- taxes_req = self._get_sii_taxes_map(["RE"])
175
+ taxes_req = self._get_sii_taxes_map(["RE"], self._get_document_fiscal_date())
366
176
  re_lines = self.line_ids.filtered(
367
177
  lambda x: tax in x.tax_ids and x.tax_ids & taxes_req
368
178
  )
@@ -402,32 +212,8 @@ class AccountMove(models.Model):
402
212
  tax_dict["CuotaRecargoEquivalencia"] = tax_lines[req_tax]["amount"]
403
213
  return tax_dict
404
214
 
405
- def _is_sii_type_breakdown_required(self, taxes_dict):
406
- """Calculates if the block 'DesgloseTipoOperacion' is required for
407
- the invoice communication."""
408
- self.ensure_one()
409
- if "DesgloseFactura" not in taxes_dict:
410
- return False
411
- country_code = self._get_sii_country_code()
412
- sii_gen_type = self._get_sii_gen_type()
413
- if "DesgloseTipoOperacion" in taxes_dict:
414
- # DesgloseTipoOperacion and DesgloseFactura are Exclusive
415
- return True
416
- elif sii_gen_type in (2, 3):
417
- # DesgloseTipoOperacion required for Intracommunity and
418
- # Export operations
419
- return True
420
- elif sii_gen_type == 1 and country_code != "ES":
421
- # DesgloseTipoOperacion required for national operations
422
- # with 'IDOtro' in the SII identifier block
423
- return True
424
- elif sii_gen_type == 1 and (self._sii_get_partner().vat or "").startswith(
425
- "ESN"
426
- ):
427
- # DesgloseTipoOperacion required if customer's country is Spain and
428
- # has a NIF which starts with 'N'
429
- return True
430
- return False
215
+ def _get_document_amount_total(self):
216
+ return self.amount_total_signed
431
217
 
432
218
  def _get_sii_out_taxes(self): # noqa: C901
433
219
  """Get the taxes for sales invoices.
@@ -436,17 +222,21 @@ class AccountMove(models.Model):
436
222
  """
437
223
  self.ensure_one()
438
224
  taxes_dict = {}
439
- taxes_sfesb = self._get_sii_taxes_map(["SFESB"])
440
- taxes_sfesbe = self._get_sii_taxes_map(["SFESBE"])
441
- taxes_sfesisp = self._get_sii_taxes_map(["SFESISP"])
225
+ taxes_sfesb = self._get_sii_taxes_map(["SFESB"], self.date)
226
+ taxes_sfesbe = self._get_sii_taxes_map(["SFESBE"], self.date)
227
+ taxes_sfesisp = self._get_sii_taxes_map(["SFESISP"], self.date)
442
228
  # taxes_sfesisps = self._get_taxes_map(['SFESISPS'])
443
- taxes_sfens = self._get_sii_taxes_map(["SFENS"])
444
- taxes_sfess = self._get_sii_taxes_map(["SFESS"])
445
- taxes_sfesse = self._get_sii_taxes_map(["SFESSE"])
446
- taxes_sfesns = self._get_sii_taxes_map(["SFESNS"])
447
- taxes_not_in_total = self._get_sii_taxes_map(["NotIncludedInTotal"])
448
- taxes_not_in_total_neg = self._get_sii_taxes_map(["NotIncludedInTotalNegative"])
449
- base_not_in_total = self._get_sii_taxes_map(["BaseNotIncludedInTotal"])
229
+ taxes_sfens = self._get_sii_taxes_map(["SFENS"], self.date)
230
+ taxes_sfess = self._get_sii_taxes_map(["SFESS"], self.date)
231
+ taxes_sfesse = self._get_sii_taxes_map(["SFESSE"], self.date)
232
+ taxes_sfesns = self._get_sii_taxes_map(["SFESNS"], self.date)
233
+ taxes_not_in_total = self._get_sii_taxes_map(["NotIncludedInTotal"], self.date)
234
+ taxes_not_in_total_neg = self._get_sii_taxes_map(
235
+ ["NotIncludedInTotalNegative"], self.date
236
+ )
237
+ base_not_in_total = self._get_sii_taxes_map(
238
+ ["BaseNotIncludedInTotal"], self.date
239
+ )
450
240
  not_in_amount_total = 0
451
241
  exempt_cause = self._get_sii_exempt_cause(taxes_sfesbe + taxes_sfesse)
452
242
  tax_lines = self._get_aeat_tax_info()
@@ -565,15 +355,19 @@ class AccountMove(models.Model):
565
355
  """
566
356
  self.ensure_one()
567
357
  taxes_dict = {}
568
- taxes_sfrs = self._get_sii_taxes_map(["SFRS"])
569
- taxes_sfrsa = self._get_sii_taxes_map(["SFRSA"])
570
- taxes_sfrisp = self._get_sii_taxes_map(["SFRISP"])
571
- taxes_sfrns = self._get_sii_taxes_map(["SFRNS"])
572
- taxes_sfrnd = self._get_sii_taxes_map(["SFRND"])
573
- taxes_sfrbi = self._get_sii_taxes_map(["SFRBI"])
574
- taxes_not_in_total = self._get_sii_taxes_map(["NotIncludedInTotal"])
575
- taxes_not_in_total_neg = self._get_sii_taxes_map(["NotIncludedInTotalNegative"])
576
- base_not_in_total = self._get_sii_taxes_map(["BaseNotIncludedInTotal"])
358
+ taxes_sfrs = self._get_sii_taxes_map(["SFRS"], self.date)
359
+ taxes_sfrsa = self._get_sii_taxes_map(["SFRSA"], self.date)
360
+ taxes_sfrisp = self._get_sii_taxes_map(["SFRISP"], self.date)
361
+ taxes_sfrns = self._get_sii_taxes_map(["SFRNS"], self.date)
362
+ taxes_sfrnd = self._get_sii_taxes_map(["SFRND"], self.date)
363
+ taxes_sfrbi = self._get_sii_taxes_map(["SFRBI"], self.date)
364
+ taxes_not_in_total = self._get_sii_taxes_map(["NotIncludedInTotal"], self.date)
365
+ taxes_not_in_total_neg = self._get_sii_taxes_map(
366
+ ["NotIncludedInTotalNegative"], self.date
367
+ )
368
+ base_not_in_total = self._get_sii_taxes_map(
369
+ ["BaseNotIncludedInTotal"], self.date
370
+ )
577
371
  tax_amount = 0.0
578
372
  not_in_amount_total = 0.0
579
373
  tax_lines = self._get_aeat_tax_info()
@@ -617,157 +411,88 @@ class AccountMove(models.Model):
617
411
  base_dict["DetalleIVA"].append(tax_dict)
618
412
  return taxes_dict, tax_amount, not_in_amount_total
619
413
 
620
- def _is_sii_simplified_invoice(self):
621
- """Inheritable method to allow control when an
622
- invoice are simplified or normal"""
623
- partner = self._sii_get_partner()
624
- is_simplified = partner.sii_simplified_invoice
625
- return is_simplified
414
+ def _get_mapping_key(self):
415
+ return self.move_type
626
416
 
627
417
  def _sii_check_exceptions(self):
628
- """Inheritable method for exceptions control when sending SII invoices."""
629
- self.ensure_one()
630
- gen_type = self._get_sii_gen_type()
631
- partner = self._sii_get_partner()
632
- country_code = self._get_sii_country_code()
418
+ res = super()._sii_check_exceptions()
633
419
  is_simplified_invoice = self._is_sii_simplified_invoice()
634
-
635
420
  if is_simplified_invoice and self.move_type[:2] == "in":
636
421
  raise exceptions.UserError(
637
422
  _("You can't make a supplier simplified invoice.")
638
423
  )
639
- if (
640
- (gen_type != 3 or country_code == "ES")
641
- and not partner.vat
642
- and not is_simplified_invoice
643
- ):
644
- raise exceptions.UserError(_("The partner has not a VAT configured."))
645
- if not self.company_id.chart_template_id:
646
- raise exceptions.UserError(
647
- _("You have to select what account chart template use this" " company.")
648
- )
649
- if not self.company_id.sii_enabled:
650
- raise exceptions.UserError(_("This company doesn't have SII enabled."))
651
- if not self.sii_enabled:
652
- raise exceptions.UserError(_("This invoice is not SII enabled."))
653
424
  if not self.ref and self.move_type in ["in_invoice", "in_refund"]:
654
425
  raise exceptions.UserError(_("The supplier number invoice is required"))
655
-
656
- def _get_account_registration_date(self):
657
- """Hook method to allow the setting of the account registration date
658
- of each supplier invoice. The SII recommends to set the send date as
659
- the default value (point 9.3 of the document
660
- SII_Descripcion_ServicioWeb_v0.7.pdf), so by default we return
661
- the current date or, if exists, the stored
662
- sii_account_registration_date
663
- :return String date in the format %Y-%m-%d"""
664
- self.ensure_one()
665
- return self.sii_account_registration_date or fields.Date.today()
426
+ return res
666
427
 
667
428
  def _get_sii_invoice_type(self):
668
- tipo_factura = ""
429
+ invoice_type = ""
669
430
  if self.sii_lc_operation:
670
431
  return "LC"
671
432
  if self.move_type in ["in_invoice", "in_refund"]:
672
- tipo_factura = "R4" if self.move_type == "in_refund" else "F1"
433
+ invoice_type = "R4" if self.move_type == "in_refund" else "F1"
673
434
  elif self.move_type in ["out_invoice", "out_refund"]:
674
435
  is_simplified = self._is_sii_simplified_invoice()
675
- tipo_factura = "F2" if is_simplified else "F1"
436
+ invoice_type = "F2" if is_simplified else "F1"
676
437
  if self.move_type == "out_refund":
677
438
  if self.sii_refund_specific_invoice_type:
678
- tipo_factura = self.sii_refund_specific_invoice_type
439
+ invoice_type = self.sii_refund_specific_invoice_type
679
440
  else:
680
- tipo_factura = "R5" if is_simplified else "R1"
681
- return tipo_factura
441
+ invoice_type = "R5" if is_simplified else "R1"
442
+ return invoice_type
682
443
 
683
444
  def _get_sii_invoice_dict_out(self, cancel=False):
684
- """Build dict with data to send to AEAT WS for invoice types:
685
- out_invoice and out_refund.
686
-
687
- :param cancel: It indicates if the dictionary is for sending a
688
- cancellation of the invoice.
689
- :return: invoices (dict) : Dict XML with data for this invoice.
690
- """
691
- self.ensure_one()
692
- invoice_date = self._change_date_format(self.invoice_date)
693
- partner = self._sii_get_partner()
694
- company = self.company_id
695
- ejercicio = fields.Date.to_date(self.date).year
696
- periodo = "%02d" % fields.Date.to_date(self.date).month
697
- is_simplified_invoice = self._is_sii_simplified_invoice()
698
- serial_number = (self.name or "")[0:60]
445
+ inv_dict = super()._get_sii_invoice_dict_out(cancel=cancel)
699
446
  if self.thirdparty_invoice:
700
- serial_number = self.thirdparty_number[0:60]
701
- inv_dict = {
702
- "IDFactura": {
703
- "IDEmisorFactura": {
704
- "NIF": company.partner_id._parse_aeat_vat_info()[2]
705
- },
706
- # On cancelled invoices, number is not filled
707
- "NumSerieFacturaEmisor": serial_number,
708
- "FechaExpedicionFacturaEmisor": invoice_date,
709
- },
710
- "PeriodoLiquidacion": {"Ejercicio": ejercicio, "Periodo": periodo},
711
- }
712
- if not cancel:
713
- tipo_desglose, not_in_amount_total = self._get_sii_out_taxes()
714
- amount_total = self.amount_total_signed - not_in_amount_total
715
- inv_dict["FacturaExpedida"] = {
716
- "TipoFactura": self._get_sii_invoice_type(),
717
- "ClaveRegimenEspecialOTrascendencia": (self.sii_registration_key.code),
718
- "DescripcionOperacion": self.sii_description,
719
- "TipoDesglose": tipo_desglose,
720
- "ImporteTotal": amount_total,
721
- }
722
- if self.thirdparty_invoice:
723
- inv_dict["FacturaExpedida"]["EmitidaPorTercerosODestinatario"] = "S"
724
- if self.sii_macrodata:
725
- inv_dict["FacturaExpedida"].update(Macrodato="S")
726
- if self.sii_registration_key_additional1:
727
- inv_dict["FacturaExpedida"].update(
728
- {
729
- "ClaveRegimenEspecialOTrascendenciaAdicional1": (
730
- self.sii_registration_key_additional1.code
731
- )
732
- }
733
- )
734
- if self.sii_registration_key_additional2:
735
- inv_dict["FacturaExpedida"].update(
736
- {
737
- "ClaveRegimenEspecialOTrascendenciaAdicional2": (
738
- self.sii_registration_key_additional2.code
739
- )
740
- }
741
- )
742
- if self.sii_registration_key.code in ["12", "13"]:
743
- inv_dict["FacturaExpedida"]["DatosInmueble"] = {
744
- "DetalleInmueble": {
745
- "SituacionInmueble": self.sii_property_location,
746
- "ReferenciaCatastral": (
747
- self.sii_property_cadastrial_code or ""
748
- ),
749
- }
447
+ inv_dict["FacturaExpedida"]["EmitidaPorTercerosODestinatario"] = "S"
448
+ if self.sii_registration_key_additional1:
449
+ inv_dict["FacturaExpedida"].update(
450
+ {
451
+ "ClaveRegimenEspecialOTrascendenciaAdicional1": (
452
+ self.sii_registration_key_additional1.code
453
+ )
750
454
  }
751
- exp_dict = inv_dict["FacturaExpedida"]
752
- if not is_simplified_invoice:
753
- # Simplified invoices don't have counterpart
754
- exp_dict["Contraparte"] = {
755
- "NombreRazon": partner.name[0:120],
455
+ )
456
+ if self.sii_registration_key_additional2:
457
+ inv_dict["FacturaExpedida"].update(
458
+ {
459
+ "ClaveRegimenEspecialOTrascendenciaAdicional2": (
460
+ self.sii_registration_key_additional2.code
461
+ )
462
+ }
463
+ )
464
+ if self.sii_registration_key.code in ["12", "13"]:
465
+ inv_dict["FacturaExpedida"]["DatosInmueble"] = {
466
+ "DetalleInmueble": {
467
+ "SituacionInmueble": self.sii_property_location,
468
+ "ReferenciaCatastral": (self.sii_property_cadastrial_code or ""),
469
+ }
470
+ }
471
+ exp_dict = inv_dict["FacturaExpedida"]
472
+ if self.move_type == "out_refund":
473
+ exp_dict["TipoRectificativa"] = self.sii_refund_type
474
+ if self.sii_refund_type == "S":
475
+ origin = self.refund_invoice_id
476
+ exp_dict["ImporteRectificacion"] = {
477
+ "BaseRectificada": abs(origin.amount_untaxed_signed),
478
+ "CuotaRectificada": abs(
479
+ origin.amount_total_signed - origin.amount_untaxed_signed
480
+ ),
756
481
  }
757
- # Uso condicional de IDOtro/NIF
758
- exp_dict["Contraparte"].update(self._get_sii_identifier())
759
- if self.move_type == "out_refund":
760
- exp_dict["TipoRectificativa"] = self.sii_refund_type
761
- if self.sii_refund_type == "S":
762
- origin = self.refund_invoice_id
763
- exp_dict["ImporteRectificacion"] = {
764
- "BaseRectificada": abs(origin.amount_untaxed_signed),
765
- "CuotaRectificada": abs(
766
- origin.amount_total_signed - origin.amount_untaxed_signed
767
- ),
768
- }
769
482
  return inv_dict
770
483
 
484
+ def _get_document_date(self):
485
+ return self.invoice_date
486
+
487
+ def _get_document_fiscal_date(self):
488
+ return self.date
489
+
490
+ def _get_document_serial_number(self):
491
+ serial_number = (self.name or "")[0:60]
492
+ if self.thirdparty_invoice:
493
+ serial_number = self.thirdparty_number[0:60]
494
+ return serial_number
495
+
771
496
  def _get_sii_invoice_dict_in(self, cancel=False):
772
497
  """Build dict with data to send to AEAT WS for invoice types:
773
498
  in_invoice and in_refund.
@@ -844,33 +569,6 @@ class AccountMove(models.Model):
844
569
  }
845
570
  return inv_dict
846
571
 
847
- def _get_sii_invoice_dict(self):
848
- self.ensure_one()
849
- self._sii_check_exceptions()
850
- inv_dict = {}
851
- if self.move_type in ["out_invoice", "out_refund"]:
852
- inv_dict = self._get_sii_invoice_dict_out()
853
- elif self.move_type in ["in_invoice", "in_refund"]:
854
- inv_dict = self._get_sii_invoice_dict_in()
855
- round_by_keys(
856
- inv_dict,
857
- [
858
- "BaseImponible",
859
- "CuotaRepercutida",
860
- "CuotaSoportada",
861
- "TipoRecargoEquivalencia",
862
- "CuotaRecargoEquivalencia",
863
- "ImportePorArticulos7_14_Otros",
864
- "ImporteTAIReglasLocalizacion",
865
- "ImporteTotal",
866
- "BaseRectificada",
867
- "CuotaRectificada",
868
- "CuotaDeducible",
869
- "ImporteCompensacionREAGYP",
870
- ],
871
- )
872
- return inv_dict
873
-
874
572
  def _get_cancel_sii_invoice_dict(self):
875
573
  self.ensure_one()
876
574
  self._sii_check_exceptions()
@@ -880,136 +578,6 @@ class AccountMove(models.Model):
880
578
  return self._get_sii_invoice_dict_in(cancel=True)
881
579
  return {}
882
580
 
883
- def _connect_params_sii(self, mapping_key):
884
- self.ensure_one()
885
- agency = self.company_id.tax_agency_id
886
- if not agency:
887
- # We use spanish agency by default to keep old behavior with
888
- # ir.config parameters. In the future it might be good to reinforce
889
- # to explicitly set a tax agency in the company by raising an error
890
- # here.
891
- agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
892
- return agency._connect_params_sii(mapping_key, self.company_id)
893
-
894
- def _connect_sii(self, mapping_key):
895
- self.ensure_one()
896
- public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates(
897
- company=self.company_id
898
- )
899
- params = self._connect_params_sii(mapping_key)
900
- session = Session()
901
- session.cert = (public_crt, private_key)
902
- transport = Transport(session=session)
903
- history = HistoryPlugin()
904
- client = Client(wsdl=params["wsdl"], transport=transport, plugins=[history])
905
- return self._bind_sii(client, params["port_name"], params["address"])
906
-
907
- def _bind_sii(self, client, port_name, address=None):
908
- self.ensure_one()
909
- service = client._get_service("siiService")
910
- port = client._get_port(service, port_name)
911
- address = address or port.binding_options["address"]
912
- return client.create_service(port.binding.name, address)
913
-
914
- def _process_invoice_for_sii_send(self):
915
- """Process invoices for sending to the SII. Adds general checks from
916
- configuration parameters and invoice availability for SII. If the
917
- invoice is to be sent the decides the send method: direct send or
918
- via connector depending on 'Use connector' configuration"""
919
- queue_obj = self.env["queue.job"].sudo()
920
- for invoice in self:
921
- company = invoice.company_id
922
- if not company.use_connector:
923
- invoice._send_invoice_to_sii()
924
- else:
925
- eta = company._get_sii_eta()
926
- new_delay = (
927
- invoice.sudo()
928
- .with_context(company_id=company.id)
929
- .with_delay(eta=eta if not invoice.sii_send_failed else False)
930
- .confirm_one_invoice()
931
- )
932
- job = queue_obj.search([("uuid", "=", new_delay.uuid)], limit=1)
933
- invoice.sudo().invoice_jobs_ids |= job
934
-
935
- def _send_invoice_to_sii(self):
936
- for invoice in self.filtered(lambda i: i.state in SII_VALID_INVOICE_STATES):
937
- serv = invoice._connect_sii(invoice.move_type)
938
- if invoice.sii_state == "not_sent":
939
- tipo_comunicacion = "A0"
940
- else:
941
- tipo_comunicacion = "A1"
942
- header = invoice._get_sii_header(tipo_comunicacion)
943
- inv_vals = {
944
- "sii_header_sent": json.dumps(header, indent=4),
945
- }
946
- try:
947
- inv_dict = invoice._get_sii_invoice_dict()
948
- inv_vals["sii_content_sent"] = json.dumps(inv_dict, indent=4)
949
- if invoice.move_type in ["out_invoice", "out_refund"]:
950
- res = serv.SuministroLRFacturasEmitidas(header, inv_dict)
951
- elif invoice.move_type in ["in_invoice", "in_refund"]:
952
- res = serv.SuministroLRFacturasRecibidas(header, inv_dict)
953
- # TODO Facturas intracomunitarias 66 RIVA
954
- # elif invoice.fiscal_position_id.id == self.env.ref(
955
- # 'account.fp_intra').id:
956
- # res = serv.SuministroLRDetOperacionIntracomunitaria(
957
- # header, invoices)
958
- res_line = res["RespuestaLinea"][0]
959
- if res["EstadoEnvio"] == "Correcto":
960
- inv_vals.update(
961
- {
962
- "sii_state": "sent",
963
- "sii_csv": res["CSV"],
964
- "sii_send_failed": False,
965
- }
966
- )
967
- elif (
968
- res["EstadoEnvio"] == "ParcialmenteCorrecto"
969
- and res_line["EstadoRegistro"] == "AceptadoConErrores"
970
- ):
971
- inv_vals.update(
972
- {
973
- "sii_state": "sent_w_errors",
974
- "sii_csv": res["CSV"],
975
- "sii_send_failed": True,
976
- }
977
- )
978
- else:
979
- inv_vals["sii_send_failed"] = True
980
- if (
981
- "sii_state" in inv_vals
982
- and not invoice.sii_account_registration_date
983
- and invoice.move_type[:2] == "in"
984
- ):
985
- inv_vals[
986
- "sii_account_registration_date"
987
- ] = self._get_account_registration_date()
988
- inv_vals["sii_return"] = res
989
- send_error = False
990
- if res_line["CodigoErrorRegistro"]:
991
- send_error = "{} | {}".format(
992
- str(res_line["CodigoErrorRegistro"]),
993
- str(res_line["DescripcionErrorRegistro"])[:60],
994
- )
995
- inv_vals["sii_send_error"] = send_error
996
- invoice.write(inv_vals)
997
- except Exception as fault:
998
- new_cr = Registry(self.env.cr.dbname).cursor()
999
- env = api.Environment(new_cr, self.env.uid, self.env.context)
1000
- invoice = env["account.move"].browse(invoice.id)
1001
- inv_vals.update(
1002
- {
1003
- "sii_send_failed": True,
1004
- "sii_send_error": repr(fault)[:60],
1005
- "sii_return": repr(fault),
1006
- }
1007
- )
1008
- invoice.write(inv_vals)
1009
- new_cr.commit()
1010
- new_cr.close()
1011
- raise
1012
-
1013
581
  def _sii_invoice_dict_not_modified(self):
1014
582
  self.ensure_one()
1015
583
  to_send = self._get_sii_invoice_dict()
@@ -1033,7 +601,7 @@ class AccountMove(models.Model):
1033
601
  company = invoice.company_id
1034
602
  if company.sii_method != "auto":
1035
603
  continue
1036
- invoice._process_invoice_for_sii_send()
604
+ invoice._process_sii_send()
1037
605
  return res
1038
606
 
1039
607
  def process_send_sii(self):
@@ -1047,22 +615,11 @@ class AccountMove(models.Model):
1047
615
  "context": self.env.context,
1048
616
  }
1049
617
 
1050
- def send_sii(self):
1051
- invoices = self.filtered(
1052
- lambda i: (
1053
- i.sii_enabled
1054
- and i.state in SII_VALID_INVOICE_STATES
1055
- and i.sii_state not in ["sent", "cancelled"]
1056
- )
1057
- )
1058
- if not invoices._cancel_invoice_jobs():
1059
- raise exceptions.UserError(
1060
- _(
1061
- "You can not communicate this invoice at this moment "
1062
- "because there is a job running!"
1063
- )
1064
- )
1065
- invoices._process_invoice_for_sii_send()
618
+ def _get_sii_jobs_field_name(self):
619
+ return "invoice_jobs_ids"
620
+
621
+ def _get_valid_document_states(self):
622
+ return SII_VALID_INVOICE_STATES
1066
623
 
1067
624
  def _cancel_invoice_to_sii(self):
1068
625
  for invoice in self.filtered(lambda i: i.state in ["cancel"]):
@@ -1123,7 +680,7 @@ class AccountMove(models.Model):
1123
680
  and i.sii_state in ["sent", "sent_w_errors", "sent_modified"]
1124
681
  )
1125
682
  )
1126
- if not invoices._cancel_invoice_jobs():
683
+ if not invoices._cancel_sii_jobs():
1127
684
  raise exceptions.UserError(
1128
685
  _(
1129
686
  "You can not communicate the cancellation of this invoice "
@@ -1146,16 +703,8 @@ class AccountMove(models.Model):
1146
703
  job = queue_obj.search([("uuid", "=", new_delay.uuid)], limit=1)
1147
704
  invoice.sudo().invoice_jobs_ids |= job
1148
705
 
1149
- def _cancel_invoice_jobs(self):
1150
- for queue in self.sudo().mapped("invoice_jobs_ids"):
1151
- if queue.state == "started":
1152
- return False
1153
- elif queue.state in ("pending", "enqueued", "failed"):
1154
- queue.unlink()
1155
- return True
1156
-
1157
706
  def button_cancel(self):
1158
- if not self._cancel_invoice_jobs():
707
+ if not self._cancel_sii_jobs():
1159
708
  raise exceptions.UserError(
1160
709
  _("You can not cancel this invoice because" " there is a job running!")
1161
710
  )
@@ -1170,7 +719,7 @@ class AccountMove(models.Model):
1170
719
  return res
1171
720
 
1172
721
  def button_draft(self):
1173
- if not self._cancel_invoice_jobs():
722
+ if not self._cancel_sii_jobs():
1174
723
  raise exceptions.UserError(
1175
724
  _(
1176
725
  "You can not set to draft this invoice because"
@@ -1179,126 +728,17 @@ class AccountMove(models.Model):
1179
728
  )
1180
729
  return super().button_draft()
1181
730
 
1182
- def _get_sii_gen_type(self):
1183
- """Make a choice for general invoice type
1184
-
1185
- Returns:
1186
- int: 1 (National), 2 (Intracom), 3 (Export)
1187
- """
1188
- self.ensure_one()
1189
- partner_ident = self.fiscal_position_id.sii_partner_identification_type
1190
- if partner_ident:
1191
- res = int(partner_ident)
1192
- elif self.fiscal_position_id.name == "Régimen Intracomunitario":
1193
- res = 2
1194
- elif self.fiscal_position_id.name == "Régimen Extracomunitario":
1195
- res = 3
1196
- else:
1197
- res = 1
1198
- return res
1199
-
1200
- def _get_sii_identifier(self):
1201
- """Get the SII structure for a partner identifier depending on the
1202
- conditions of the invoice.
1203
- """
1204
- self.ensure_one()
1205
- gen_type = self._get_sii_gen_type()
1206
- (
1207
- country_code,
1208
- identifier_type,
1209
- identifier,
1210
- ) = self._sii_get_partner()._parse_aeat_vat_info()
1211
- # Limpiar alfanum
1212
- if identifier:
1213
- identifier = "".join(e for e in identifier if e.isalnum()).upper()
1214
- else:
1215
- identifier = "NO_DISPONIBLE"
1216
- identifier_type = "06"
1217
- if gen_type == 1:
1218
- if "1117" in (self.sii_send_error or ""):
1219
- return {
1220
- "IDOtro": {
1221
- "CodigoPais": country_code,
1222
- "IDType": "07",
1223
- "ID": identifier,
1224
- }
1225
- }
1226
- else:
1227
- if identifier_type == "":
1228
- return {"NIF": identifier}
1229
- return {
1230
- "IDOtro": {
1231
- "CodigoPais": country_code,
1232
- "IDType": identifier_type,
1233
- "ID": country_code + identifier
1234
- if self.commercial_partner_id._map_aeat_country_code(
1235
- country_code
1236
- )
1237
- in self.commercial_partner_id._get_aeat_europe_codes()
1238
- else identifier,
1239
- },
1240
- }
1241
- elif gen_type == 2:
1242
- return {"IDOtro": {"IDType": "02", "ID": country_code + identifier}}
1243
- elif gen_type == 3 and identifier_type:
1244
- # Si usamos identificador tipo 02 en exportaciones, el envío falla con:
1245
- # {'CodigoErrorRegistro': 1104,
1246
- # 'DescripcionErrorRegistro': 'Valor del campo ID incorrecto'}
1247
- if identifier_type == "02":
1248
- identifier_type = "06"
1249
- return {
1250
- "IDOtro": {
1251
- "CodigoPais": country_code,
1252
- "IDType": identifier_type,
1253
- "ID": identifier,
1254
- },
1255
- }
1256
- elif gen_type == 3:
1257
- return {"NIF": identifier}
1258
-
1259
- def _get_sii_exempt_cause(self, applied_taxes):
1260
- """Código de la causa de exención según 3.6 y 3.7 de la FAQ del SII.
1261
-
1262
- :param applied_taxes: Taxes that are exempt for filtering the lines.
1263
- """
1264
- self.ensure_one()
1265
- gen_type = self._get_sii_gen_type()
1266
- if gen_type == 2:
1267
- return "E5"
1268
- else:
1269
- exempt_cause = False
1270
- product_exempt_causes = (
1271
- self.mapped("invoice_line_ids")
1272
- .filtered(
1273
- lambda x: (
1274
- any(tax in x.tax_ids for tax in applied_taxes)
1275
- and x.product_id.sii_exempt_cause
1276
- and x.product_id.sii_exempt_cause != "none"
1277
- )
731
+ def _get_document_product_exempt(self, applied_taxes):
732
+ return set(
733
+ self.mapped("invoice_line_ids")
734
+ .filtered(
735
+ lambda x: (
736
+ any(tax in x.tax_ids for tax in applied_taxes)
737
+ and x.product_id.sii_exempt_cause
738
+ and x.product_id.sii_exempt_cause != "none"
1278
739
  )
1279
- .mapped("product_id.sii_exempt_cause")
1280
740
  )
1281
- product_exempt_causes = set(product_exempt_causes)
1282
- if len(product_exempt_causes) > 1:
1283
- raise exceptions.UserError(
1284
- _("Currently there's no support for multiple exempt " "causes.")
1285
- )
1286
- if product_exempt_causes:
1287
- exempt_cause = product_exempt_causes.pop()
1288
- elif (
1289
- self.fiscal_position_id.sii_exempt_cause
1290
- and self.fiscal_position_id.sii_exempt_cause != "none"
1291
- ):
1292
- exempt_cause = self.fiscal_position_id.sii_exempt_cause
1293
- if gen_type == 3 and exempt_cause not in ["E2", "E3"]:
1294
- exempt_cause = "E2"
1295
- return exempt_cause
1296
-
1297
- def _get_no_taxable_cause(self):
1298
- self.ensure_one()
1299
- return (
1300
- self.fiscal_position_id.sii_no_taxable_cause
1301
- or "ImporteTAIReglasLocalizacion"
741
+ .mapped("product_id.sii_exempt_cause")
1302
742
  )
1303
743
 
1304
744
  def is_sii_invoice(self):
@@ -1310,10 +750,6 @@ class AccountMove(models.Model):
1310
750
  """
1311
751
  self.ensure_one()
1312
752
 
1313
- def _get_sii_country_code(self):
1314
- self.ensure_one()
1315
- return self._sii_get_partner()._parse_aeat_vat_info()[0]
1316
-
1317
753
  @api.depends(
1318
754
  "invoice_line_ids",
1319
755
  "invoice_line_ids.name",
@@ -1386,8 +822,5 @@ class AccountMove(models.Model):
1386
822
  )
1387
823
  return res
1388
824
 
1389
- def confirm_one_invoice(self):
1390
- self.sudo()._send_invoice_to_sii()
1391
-
1392
825
  def cancel_one_invoice(self):
1393
826
  self.sudo()._cancel_invoice_to_sii()