odoo-addon-l10n-es-aeat-sii-oca 16.0.2.5.3__py3-none-any.whl → 17.0.1.1.0__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 (74) hide show
  1. odoo/addons/l10n_es_aeat_sii_oca/README.rst +73 -86
  2. odoo/addons/l10n_es_aeat_sii_oca/__manifest__.py +7 -6
  3. odoo/addons/l10n_es_aeat_sii_oca/data/aeat_sii_map_data.xml +208 -187
  4. odoo/addons/l10n_es_aeat_sii_oca/data/aeat_sii_tax_agency_data.xml +0 -25
  5. odoo/addons/l10n_es_aeat_sii_oca/data/ir_config_parameter_data.xml +9 -0
  6. odoo/addons/l10n_es_aeat_sii_oca/data/ir_cron.xml +12 -0
  7. odoo/addons/l10n_es_aeat_sii_oca/data/l10n.es.aeat.map.tax.line.tax.csv +143 -0
  8. odoo/addons/l10n_es_aeat_sii_oca/hooks.py +3 -6
  9. odoo/addons/l10n_es_aeat_sii_oca/i18n/bg.po +0 -33
  10. odoo/addons/l10n_es_aeat_sii_oca/i18n/ca.po +0 -33
  11. odoo/addons/l10n_es_aeat_sii_oca/i18n/cs.po +0 -33
  12. odoo/addons/l10n_es_aeat_sii_oca/i18n/de.po +0 -33
  13. odoo/addons/l10n_es_aeat_sii_oca/i18n/es.po +3 -33
  14. odoo/addons/l10n_es_aeat_sii_oca/i18n/es_CO.po +0 -33
  15. odoo/addons/l10n_es_aeat_sii_oca/i18n/es_CR.po +0 -33
  16. odoo/addons/l10n_es_aeat_sii_oca/i18n/eu.po +0 -33
  17. odoo/addons/l10n_es_aeat_sii_oca/i18n/fr.po +0 -33
  18. odoo/addons/l10n_es_aeat_sii_oca/i18n/gl.po +0 -33
  19. odoo/addons/l10n_es_aeat_sii_oca/i18n/hr.po +0 -33
  20. odoo/addons/l10n_es_aeat_sii_oca/i18n/l10n_es_aeat_sii_oca.pot +92 -82
  21. odoo/addons/l10n_es_aeat_sii_oca/i18n/nl.po +0 -33
  22. odoo/addons/l10n_es_aeat_sii_oca/i18n/pl.po +0 -33
  23. odoo/addons/l10n_es_aeat_sii_oca/i18n/pt.po +0 -33
  24. odoo/addons/l10n_es_aeat_sii_oca/i18n/pt_BR.po +0 -33
  25. odoo/addons/l10n_es_aeat_sii_oca/i18n/ru.po +0 -33
  26. odoo/addons/l10n_es_aeat_sii_oca/i18n/sl.po +0 -33
  27. odoo/addons/l10n_es_aeat_sii_oca/i18n/sv.po +0 -33
  28. odoo/addons/l10n_es_aeat_sii_oca/i18n/tr.po +0 -33
  29. odoo/addons/l10n_es_aeat_sii_oca/i18n/vi.po +0 -33
  30. odoo/addons/l10n_es_aeat_sii_oca/models/__init__.py +0 -1
  31. odoo/addons/l10n_es_aeat_sii_oca/models/account_move.py +142 -49
  32. odoo/addons/l10n_es_aeat_sii_oca/models/aeat_sii_map.py +3 -1
  33. odoo/addons/l10n_es_aeat_sii_oca/models/aeat_sii_mapping_registration_keys.py +1 -1
  34. odoo/addons/l10n_es_aeat_sii_oca/models/res_company.py +8 -9
  35. odoo/addons/l10n_es_aeat_sii_oca/models/sii_mixin.py +60 -58
  36. odoo/addons/l10n_es_aeat_sii_oca/readme/CONFIGURE.md +20 -0
  37. odoo/addons/l10n_es_aeat_sii_oca/readme/CONTRIBUTORS.md +24 -0
  38. odoo/addons/l10n_es_aeat_sii_oca/readme/DESCRIPTION.md +2 -0
  39. odoo/addons/l10n_es_aeat_sii_oca/readme/INSTALL.md +6 -0
  40. odoo/addons/l10n_es_aeat_sii_oca/readme/ROADMAP.md +12 -0
  41. odoo/addons/l10n_es_aeat_sii_oca/readme/{USAGE.rst → USAGE.md} +2 -2
  42. odoo/addons/l10n_es_aeat_sii_oca/security/ir.model.access.csv +0 -1
  43. odoo/addons/l10n_es_aeat_sii_oca/static/description/index.html +33 -46
  44. odoo/addons/l10n_es_aeat_sii_oca/tests/json/sii_in_invoice_p_iva21_ibc_group_dict.json +24 -0
  45. odoo/addons/l10n_es_aeat_sii_oca/tests/test_l10n_es_aeat_sii.py +71 -50
  46. odoo/addons/l10n_es_aeat_sii_oca/views/account_fiscal_position_view.xml +6 -16
  47. odoo/addons/l10n_es_aeat_sii_oca/views/account_journal_view.xml +2 -4
  48. odoo/addons/l10n_es_aeat_sii_oca/views/account_move_views.xml +39 -70
  49. odoo/addons/l10n_es_aeat_sii_oca/views/aeat_sii_map_view.xml +1 -1
  50. odoo/addons/l10n_es_aeat_sii_oca/views/res_company_view.xml +6 -10
  51. odoo/addons/l10n_es_aeat_sii_oca/wizards/account_move_reversal.py +9 -8
  52. odoo/addons/l10n_es_aeat_sii_oca/wizards/account_move_reversal_views.xml +4 -2
  53. odoo/addons/l10n_es_aeat_sii_oca/wizards/account_move_send_sii.py +15 -19
  54. odoo/addons/l10n_es_aeat_sii_oca/wizards/account_move_send_sii_views.xml +38 -5
  55. {odoo_addon_l10n_es_aeat_sii_oca-16.0.2.5.3.dist-info → odoo_addon_l10n_es_aeat_sii_oca-17.0.1.1.0.dist-info}/METADATA +84 -96
  56. odoo_addon_l10n_es_aeat_sii_oca-17.0.1.1.0.dist-info/RECORD +93 -0
  57. {odoo_addon_l10n_es_aeat_sii_oca-16.0.2.5.3.dist-info → odoo_addon_l10n_es_aeat_sii_oca-17.0.1.1.0.dist-info}/WHEEL +1 -1
  58. odoo_addon_l10n_es_aeat_sii_oca-17.0.1.1.0.dist-info/top_level.txt +1 -0
  59. odoo/addons/l10n_es_aeat_sii_oca/data/aeat_sii_queue_job.xml +0 -17
  60. odoo/addons/l10n_es_aeat_sii_oca/migrations/16.0.1.4.0/post-migration.py +0 -18
  61. odoo/addons/l10n_es_aeat_sii_oca/models/queue_job.py +0 -18
  62. odoo/addons/l10n_es_aeat_sii_oca/readme/CONFIGURE.rst +0 -39
  63. odoo/addons/l10n_es_aeat_sii_oca/readme/CONTRIBUTORS.rst +0 -25
  64. odoo/addons/l10n_es_aeat_sii_oca/readme/DESCRIPTION.rst +0 -2
  65. odoo/addons/l10n_es_aeat_sii_oca/readme/INSTALL.rst +0 -8
  66. odoo/addons/l10n_es_aeat_sii_oca/readme/ROADMAP.rst +0 -10
  67. odoo/addons/l10n_es_aeat_sii_oca/security/aeat_sii.xml +0 -17
  68. odoo/addons/l10n_es_aeat_sii_oca/views/queue_job_views.xml +0 -38
  69. odoo_addon_l10n_es_aeat_sii_oca-16.0.2.5.3.dist-info/RECORD +0 -94
  70. odoo_addon_l10n_es_aeat_sii_oca-16.0.2.5.3.dist-info/top_level.txt +0 -1
  71. /odoo/addons/l10n_es_aeat_sii_oca/tests/json/{sii_out_invoice_s_iva0_sp_i_s_iva0_ic_dict.json → sii_out_invoice_s_iva0_sp_i_s_iva0_g_i_dict.json} +0 -0
  72. /odoo/addons/l10n_es_aeat_sii_oca/tests/json/{sii_out_invoice_s_iva_e_s_iva0_e_dict.json → sii_out_invoice_s_iva_e_s_iva0_g_e_dict.json} +0 -0
  73. /odoo/addons/l10n_es_aeat_sii_oca/tests/json/{sii_out_refund_s_iva0_sp_i_s_iva0_ic_dict.json → sii_out_refund_s_iva0_sp_i_s_iva0_g_i_dict.json} +0 -0
  74. /odoo/addons/l10n_es_aeat_sii_oca/tests/json/{sii_out_refund_s_iva_e_s_iva0_e_dict.json → sii_out_refund_s_iva_e_s_iva0_g_e_dict.json} +0 -0
@@ -23,18 +23,6 @@ SII_VALID_INVOICE_STATES = ["posted"]
23
23
  _logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
- try:
27
- from odoo.addons.queue_job.job import job
28
- except ImportError:
29
- _logger.debug("Can not `import queue_job`.")
30
- import functools
31
-
32
- def empty_decorator_factory(*argv, **kwargs):
33
- return functools.partial
34
-
35
- job = empty_decorator_factory
36
-
37
-
38
26
  class AccountMove(models.Model):
39
27
  _name = "account.move"
40
28
  _inherit = ["account.move", "sii.mixin"]
@@ -95,14 +83,7 @@ class AccountMove(models.Model):
95
83
  "The invoice number should start with LC, QZC, QRC, A01 or A02.",
96
84
  copy=False,
97
85
  )
98
- invoice_jobs_ids = fields.Many2many(
99
- comodel_name="queue.job",
100
- column1="invoice_id",
101
- column2="job_id",
102
- relation="account_move_queue_job_rel",
103
- string="Connector Jobs",
104
- copy=False,
105
- )
86
+ sii_dua_invoice = fields.Boolean(compute="_compute_dua_invoice")
106
87
 
107
88
  @api.depends("move_type")
108
89
  def _compute_sii_refund_type(self):
@@ -124,6 +105,21 @@ class AccountMove(models.Model):
124
105
  def _compute_macrodata(self):
125
106
  return super()._compute_macrodata()
126
107
 
108
+ @api.depends("company_id", "fiscal_position_id", "invoice_line_ids.tax_ids")
109
+ def _compute_dua_invoice(self):
110
+ for invoice in self:
111
+ taxes = self.env["account.tax"]
112
+ for template in [
113
+ "account_tax_template_p_iva4_ibc_group",
114
+ "account_tax_template_p_iva10_ibc_group",
115
+ "account_tax_template_p_iva21_ibc_group",
116
+ ]:
117
+ tax_id = invoice.company_id._get_tax_id_from_xmlid(template)
118
+ taxes |= self.env["account.tax"].browse(tax_id)
119
+ invoice.sii_dua_invoice = invoice.line_ids.filtered(
120
+ lambda x, taxes=taxes: any([tax in taxes for tax in x.tax_ids])
121
+ )
122
+
127
123
  def _aeat_get_partner(self):
128
124
  return self.commercial_partner_id
129
125
 
@@ -528,18 +524,27 @@ class AccountMove(models.Model):
528
524
  {"NombreRazon": partner.name[0:120]}
529
525
  )
530
526
  else:
531
- amount_total = -self.amount_total_signed - not_in_amount_total
527
+ invoice_type = self._get_sii_invoice_type()
528
+ company_name = partner.name[0:120]
529
+ if self.sii_dua_invoice:
530
+ company_name = self.company_id.name
531
+ if not self.sii_lc_operation:
532
+ invoice_type = "F5"
533
+
532
534
  inv_dict["FacturaRecibida"] = {
533
535
  # TODO: Incluir los 5 tipos de facturas rectificativas
534
- "TipoFactura": self._get_sii_invoice_type(),
536
+ "TipoFactura": invoice_type,
535
537
  "ClaveRegimenEspecialOTrascendencia": self.sii_registration_key.code,
536
538
  "DescripcionOperacion": self.sii_description,
537
539
  "DesgloseFactura": desglose_factura,
538
- "Contraparte": {"NombreRazon": partner.name[0:120]},
540
+ "Contraparte": {"NombreRazon": company_name},
539
541
  "FechaRegContable": reg_date,
540
- "ImporteTotal": amount_total,
541
542
  "CuotaDeducible": tax_amount,
542
543
  }
544
+ if not self.sii_dua_invoice:
545
+ inv_dict["FacturaRecibida"]["ImporteTotal"] = (
546
+ -self.amount_total_signed - not_in_amount_total
547
+ )
543
548
  if self.sii_macrodata:
544
549
  inv_dict["FacturaRecibida"].update(Macrodato="S")
545
550
  if self.sii_registration_key_additional1:
@@ -571,6 +576,13 @@ class AccountMove(models.Model):
571
576
  ),
572
577
  "CuotaRectificada": refund_tax_amount,
573
578
  }
579
+
580
+ if self.sii_dua_invoice:
581
+ inv_dict["FacturaRecibida"].pop("FechaOperacion", None)
582
+ nif = self.company_id.partner_id._parse_aeat_vat_info()[2]
583
+ inv_dict["FacturaRecibida"]["IDEmisorFactura"] = {"NIF": nif}
584
+ inv_dict["IDFactura"]["IDEmisorFactura"] = {"NIF": nif}
585
+ inv_dict["FacturaRecibida"]["Contraparte"]["NIF"] = nif
574
586
  return inv_dict
575
587
 
576
588
  def _get_cancel_sii_invoice_dict(self):
@@ -620,9 +632,7 @@ class AccountMove(models.Model):
620
632
  "context": self.env.context,
621
633
  }
622
634
 
623
- def _get_sii_jobs_field_name(self):
624
- return "invoice_jobs_ids"
625
-
635
+ @api.model
626
636
  def _get_valid_document_states(self):
627
637
  return SII_VALID_INVOICE_STATES
628
638
 
@@ -652,6 +662,7 @@ class AccountMove(models.Model):
652
662
  "aeat_state": "cancelled",
653
663
  "sii_csv": res["CSV"],
654
664
  "aeat_send_failed": False,
665
+ "sii_needs_cancel": False,
655
666
  }
656
667
  )
657
668
  res_line = res["RespuestaLinea"][0]
@@ -685,33 +696,32 @@ class AccountMove(models.Model):
685
696
  and i.aeat_state in ["sent", "sent_w_errors", "sent_modified"]
686
697
  )
687
698
  )
688
- if not invoices._cancel_sii_jobs():
699
+ if not invoices._cancel_send_to_sii():
689
700
  raise exceptions.UserError(
690
701
  _(
691
702
  "You can not communicate the cancellation of this invoice "
692
- "at this moment because there is a job running!"
703
+ "at this moment. Please, try again later."
693
704
  )
694
705
  )
695
- queue_obj = self.env["queue.job"]
696
706
  for invoice in invoices:
697
707
  company = invoice.company_id
698
- if not company.use_connector:
699
- invoice._cancel_invoice_to_sii()
700
- else:
701
- eta = company._get_sii_eta()
702
- new_delay = (
703
- invoice.sudo()
704
- .with_context(company_id=company.id)
705
- .with_delay(eta=eta)
706
- .cancel_one_invoice()
708
+ sii_sending_time = company._get_sii_sending_time()
709
+ invoice.write({"sii_send_date": sii_sending_time, "sii_needs_cancel": True})
710
+ # Create trigger if any company needs to send doc to SII now
711
+ # so the sending to SII cron is executed as soon as possible
712
+ if invoices.company_id.filtered(
713
+ lambda company: company.send_mode == "auto"
714
+ or (company.send_mode == "delayed" and company.delay_time == 0.0)
715
+ ):
716
+ sii_send_cron = self.env.ref("l10n_es_aeat_sii_oca.invoice_send_to_sii")
717
+ self.env["ir.cron.trigger"].sudo().create(
718
+ {"cron_id": sii_send_cron.id, "call_at": fields.Datetime.now()}
707
719
  )
708
- job = queue_obj.search([("uuid", "=", new_delay.uuid)], limit=1)
709
- invoice.sudo().invoice_jobs_ids |= job
710
720
 
711
721
  def button_cancel(self):
712
- if not self._cancel_sii_jobs():
722
+ if not self._cancel_send_to_sii():
713
723
  raise exceptions.UserError(
714
- _("You can not cancel this invoice because" " there is a job running!")
724
+ _("You cannot cancel this invoice. Please, try again later.")
715
725
  )
716
726
  res = super().button_cancel()
717
727
  for invoice in self.filtered(lambda x: x.sii_enabled):
@@ -724,11 +734,11 @@ class AccountMove(models.Model):
724
734
  return res
725
735
 
726
736
  def button_draft(self):
727
- if not self._cancel_sii_jobs():
737
+ if not self._cancel_send_to_sii():
728
738
  raise exceptions.UserError(
729
739
  _(
730
740
  "You can not set to draft this invoice because"
731
- " there is a job running!"
741
+ " the SII trigger could not be cancelled."
732
742
  )
733
743
  )
734
744
  return super().button_draft()
@@ -795,19 +805,31 @@ class AccountMove(models.Model):
795
805
  "move_type",
796
806
  "fiscal_position_id",
797
807
  "fiscal_position_id.aeat_active",
808
+ "invoice_line_ids",
798
809
  )
799
810
  def _compute_sii_enabled(self):
800
811
  """Compute if the invoice is enabled for the SII"""
801
812
  for invoice in self:
813
+ dua_sii_exempt_taxes = invoice._get_dua_sii_exempt_taxes()
802
814
  if (
803
815
  invoice.company_id.sii_enabled
804
816
  and invoice.journal_id.sii_enabled
805
817
  and invoice.is_invoice()
806
818
  ):
807
819
  invoice.sii_enabled = (
808
- invoice.fiscal_position_id
809
- and invoice.fiscal_position_id.aeat_active
810
- ) or not invoice.fiscal_position_id
820
+ (
821
+ invoice.fiscal_position_id
822
+ and invoice.fiscal_position_id.aeat_active
823
+ )
824
+ or not invoice.fiscal_position_id
825
+ ) and (
826
+ not dua_sii_exempt_taxes
827
+ or not invoice.invoice_line_ids.filtered(
828
+ lambda x, dua_taxes=dua_sii_exempt_taxes: any(
829
+ [tax.id in dua_taxes for tax in x.tax_ids]
830
+ )
831
+ )
832
+ )
811
833
  else:
812
834
  invoice.sii_enabled = False
813
835
 
@@ -832,7 +854,7 @@ class AccountMove(models.Model):
832
854
  # OVERRIDE
833
855
  if not default_values_list:
834
856
  default_values_list = [{} for move in self]
835
- for move, default_values in zip(self, default_values_list):
857
+ for move, default_values in zip(self, default_values_list, strict=False):
836
858
  if move.sii_enabled:
837
859
  extra_dict = {}
838
860
  sii_refund_type = self.env.context.get("sii_refund_type", False)
@@ -853,3 +875,74 @@ class AccountMove(models.Model):
853
875
 
854
876
  def cancel_one_invoice(self):
855
877
  self.sudo()._cancel_invoice_to_sii()
878
+
879
+ @api.model
880
+ def _get_sii_batch(self):
881
+ try:
882
+ return int(
883
+ self.env["ir.config_parameter"]
884
+ .sudo()
885
+ .get_param("l10n_es_aeat_sii_oca.sii_batch", "50")
886
+ )
887
+ except ValueError as e:
888
+ raise exceptions.UserError(
889
+ _(
890
+ "The value in l10n_es_aeat_sii_oca.sii_batch system"
891
+ " parameter must be an integer. Please, check the "
892
+ "value of the parameter."
893
+ )
894
+ ) from e
895
+
896
+ @api.model
897
+ def _send_to_sii_valid(self):
898
+ remaining_documents = self.env["account.move"]
899
+ documents = all_documents = self.search(
900
+ [
901
+ ("state", "in", self._get_valid_document_states()),
902
+ (
903
+ "aeat_state",
904
+ "not in",
905
+ ["sent", "cancelled"],
906
+ ),
907
+ ("sii_send_date", "<=", fields.Datetime.now()),
908
+ ]
909
+ )
910
+ if documents:
911
+ batch = self._get_sii_batch()
912
+ documents = all_documents[:batch]
913
+ remaining_documents = all_documents - documents
914
+ documents.confirm_one_document()
915
+ return remaining_documents
916
+
917
+ @api.model
918
+ def _send_to_sii_cancel(self):
919
+ remaining_cancel_documents = self.env["account.move"]
920
+ cancel_documents = all_cancel_documents = self.search(
921
+ [
922
+ ("state", "in", ["cancel"]),
923
+ (
924
+ "aeat_state",
925
+ "in",
926
+ ["sent", "sent_w_errors", "sent_modified"],
927
+ ),
928
+ ("sii_needs_cancel", "=", True),
929
+ ("sii_send_date", "<=", fields.Datetime.now()),
930
+ ]
931
+ )
932
+ if cancel_documents:
933
+ batch = self._get_sii_batch()
934
+ cancel_documents = all_cancel_documents[:batch]
935
+ remaining_cancel_documents = all_cancel_documents - cancel_documents
936
+ cancel_documents.cancel_one_invoice()
937
+ return remaining_cancel_documents
938
+
939
+ @api.model
940
+ def _send_to_sii(self):
941
+ remaining_documents = self._send_to_sii_valid()
942
+ remaining_cancel_documents = self._send_to_sii_cancel()
943
+ # Manage remaining invoices
944
+ if remaining_documents or remaining_cancel_documents:
945
+ sii_send_cron = self.env.ref("l10n_es_aeat_sii_oca.invoice_send_to_sii")
946
+ self.env["ir.cron.trigger"].sudo().create(
947
+ {"cron_id": sii_send_cron.id, "call_at": fields.Datetime.now()}
948
+ )
@@ -59,7 +59,9 @@ class AeatSiiMapLines(models.Model):
59
59
 
60
60
  code = fields.Char(required=True)
61
61
  name = fields.Char()
62
- taxes = fields.Many2many(comodel_name="account.tax.template")
62
+ tax_xmlid_ids = fields.Many2many(
63
+ comodel_name="l10n.es.aeat.map.tax.line.tax", string="Taxes templates"
64
+ )
63
65
  sii_map_id = fields.Many2one(
64
66
  comodel_name="aeat.sii.map", string="Aeat SII Map", ondelete="cascade"
65
67
  )
@@ -18,6 +18,6 @@ class AeatSiiMappingRegistrationKeys(models.Model):
18
18
  def name_get(self):
19
19
  vals = []
20
20
  for record in self:
21
- name = "[{}]-{}".format(record.code, record.name)
21
+ name = f"[{record.code}]-{record.name}"
22
22
  vals.append(tuple([record.id, name]))
23
23
  return vals
@@ -55,17 +55,13 @@ class ResCompany(models.Model):
55
55
  help="By default, the invoice is sent/queued in validation process. "
56
56
  "With manual method, there's a button to send the invoice.",
57
57
  )
58
- use_connector = fields.Boolean(
59
- help="Check it to use connector instead of sending the invoice "
60
- "directly when it's validated",
61
- )
62
58
  send_mode = fields.Selection(
63
59
  selection=[
64
60
  ("auto", "On validate"),
65
61
  ("fixed", "At fixed time"),
66
62
  ("delayed", "With delay"),
67
63
  ],
68
- default="auto",
64
+ default="delayed",
69
65
  )
70
66
  sent_time = fields.Float()
71
67
  delay_time = fields.Float()
@@ -77,7 +73,7 @@ class ResCompany(models.Model):
77
73
  default="monthly",
78
74
  )
79
75
 
80
- def _get_sii_eta(self):
76
+ def _get_sii_sending_time(self):
81
77
  if self.send_mode == "fixed":
82
78
  tz = self.env.context.get("tz", self.env.user.partner_id.tz)
83
79
  offset = datetime.now(pytz.timezone(tz)).strftime("%z") if tz else "+00"
@@ -90,7 +86,10 @@ class ResCompany(models.Model):
90
86
  now += timedelta(days=1)
91
87
  now = now.replace(hour=hour, minute=minute)
92
88
  return now
93
- elif self.send_mode == "delayed":
94
- return datetime.now() + timedelta(seconds=self.delay_time * 3600)
95
89
  else:
96
- return None
90
+ delay_time = (
91
+ 0.0
92
+ if self.send_mode == "auto" or not self.delay_time
93
+ else self.delay_time
94
+ )
95
+ return datetime.now() + timedelta(hours=delay_time)
@@ -96,6 +96,8 @@ class SiiMixin(models.AbstractModel):
96
96
  "greater o equal to 100 000 000,00 euros.",
97
97
  compute="_compute_macrodata",
98
98
  )
99
+ sii_send_date = fields.Datetime(string="SII Send Date", index=True)
100
+ sii_needs_cancel = fields.Boolean(readonly=True)
99
101
 
100
102
  def _compute_sii_refund_type(self):
101
103
  self.sii_refund_type = False
@@ -139,13 +141,6 @@ class SiiMixin(models.AbstractModel):
139
141
 
140
142
  @api.depends("sii_registration_key")
141
143
  def _compute_sii_registration_key_code(self):
142
- """
143
- Para evitar tiempos de instalación largos en BBDD grandes, es necesario que
144
- sólo dependa de sii_registration_key, ya que en caso de añadirlo odoo buscará
145
- todos los movimientos y cuando escribamos el key, aunque sea un campo no almacenado
146
- A partir de v16.0 este cambio ya no es necesario, ya que el sistema ya revisa que el
147
- campo sea almacenado o que este visualizandose (en caché)
148
- """
149
144
  for record in self:
150
145
  record.sii_registration_key_code = record.sii_registration_key.code
151
146
 
@@ -206,8 +201,24 @@ class SiiMixin(models.AbstractModel):
206
201
  ],
207
202
  limit=1,
208
203
  )
209
- tax_templates = sii_map.map_lines.filtered(lambda x: x.code in codes).taxes
210
- return self.company_id.get_taxes_from_templates(tax_templates)
204
+ tax_templates = sii_map.map_lines.filtered(
205
+ lambda x: x.code in codes
206
+ ).tax_xmlid_ids
207
+ taxes = self.env["account.tax"]
208
+ for template in tax_templates:
209
+ tax_id = self.company_id._get_tax_id_from_xmlid(template.name)
210
+ taxes |= self.env["account.tax"].browse(tax_id)
211
+ return taxes
212
+
213
+ def _get_dua_sii_exempt_taxes(self):
214
+ self.ensure_one()
215
+ taxes = []
216
+ dua_exempt_tax = self.company_id._get_tax_id_from_xmlid(
217
+ "account_tax_template_p_dua_exempt"
218
+ )
219
+ if dua_exempt_tax:
220
+ taxes.append(dua_exempt_tax)
221
+ return taxes
211
222
 
212
223
  def _get_aeat_header(self, tipo_comunicacion=False, cancellation=False):
213
224
  """Builds SII send header
@@ -233,61 +244,52 @@ class SiiMixin(models.AbstractModel):
233
244
  header.update({"TipoComunicacion": tipo_comunicacion})
234
245
  return header
235
246
 
236
- def _get_sii_jobs_field_name(self):
237
- raise NotImplementedError()
238
-
239
- def _cancel_sii_jobs(self):
240
- for queue in self.sudo().mapped(self._get_sii_jobs_field_name()):
241
- if queue.state == "started":
242
- return False
243
- elif queue.state in ("pending", "enqueued", "failed"):
244
- queue.unlink()
247
+ def _cancel_send_to_sii(self):
248
+ try:
249
+ self.sudo().write({"sii_send_date": False})
250
+ except Exception:
251
+ return False
245
252
  return True
246
253
 
247
254
  def send_sii(self):
248
- """General public method for filtering out of the starting recordset the records
249
- that shouldn't be sent to the SII:
250
-
251
- - Documents of companies with SII not enabled (through sii_enabled).
252
- - Documents not applicable to be sent to SII (through sii_enabled).
253
- - Documents in non applicable states (for example, cancelled invoices).
254
- - Documents already sent to the SII.
255
- - Documents with sending jobs pending to be executed.
256
- """
257
- valid_states = self._get_valid_document_states()
258
- jobs_field_name = self._get_sii_jobs_field_name()
259
- for document in self:
260
- if (
261
- not document.sii_enabled
262
- or document.state not in valid_states
263
- or document.aeat_state in ["sent", "cancelled"]
264
- ):
265
- continue
266
- job_states = document.sudo().mapped(jobs_field_name).mapped("state")
267
- if any([x in ("started", "pending", "enqueued") for x in job_states]):
268
- continue
269
- document._process_sii_send()
255
+ documents = self.filtered(
256
+ lambda document: (
257
+ document.sii_enabled
258
+ and document.state in self._get_valid_document_states()
259
+ and document.aeat_state not in ["sent", "cancelled"]
260
+ )
261
+ )
262
+ if not documents._cancel_send_to_sii():
263
+ raise UserError(
264
+ _(
265
+ "You can not communicate this document at this moment. "
266
+ "Please, try again later."
267
+ )
268
+ )
269
+ if documents:
270
+ documents.with_context(bypass_sii_send=True)._process_sii_send()
271
+ sii_send_cron = self.env.ref("l10n_es_aeat_sii_oca.invoice_send_to_sii")
272
+ self.env["ir.cron.trigger"].sudo().create(
273
+ {"cron_id": sii_send_cron.id, "call_at": fields.Datetime.now()}
274
+ )
270
275
 
271
276
  def _process_sii_send(self):
272
277
  """Process document sending to the SII. Adds general checks from
273
- configuration parameters and document availability for SII. If the
274
- document is to be sent the decides the send method: direct send or
275
- via connector depending on 'Use connector' configuration"""
276
- queue_obj = self.env["queue.job"].sudo()
278
+ configuration parameters and document availability for SII."""
277
279
  for record in self:
278
280
  company = record.company_id
279
- if not company.use_connector:
280
- record.confirm_one_document()
281
- else:
282
- eta = company._get_sii_eta()
283
- new_delay = (
284
- record.sudo()
285
- .with_context(company_id=company.id)
286
- .with_delay(eta=eta if not record.aeat_send_failed else False)
287
- .confirm_one_document()
288
- )
289
- job = queue_obj.search([("uuid", "=", new_delay.uuid)], limit=1)
290
- setattr(record.sudo(), self._get_sii_jobs_field_name(), [(4, job.id)])
281
+ sii_sending_time = company._get_sii_sending_time()
282
+ record.sii_send_date = sii_sending_time
283
+ # Create trigger if any company needs to send doc to SII now
284
+ # so the sending to SII cron is executed as soon as possible
285
+ if self.company_id.filtered(
286
+ lambda company: company.send_mode == "auto"
287
+ or (company.send_mode == "delayed" and company.delay_time == 0.0)
288
+ ) and not self.env.context.get("bypass_sii_send", False):
289
+ sii_send_cron = self.env.ref("l10n_es_aeat_sii_oca.invoice_send_to_sii")
290
+ self.env["ir.cron.trigger"].sudo().create(
291
+ {"cron_id": sii_send_cron.id, "call_at": fields.Datetime.now()}
292
+ )
291
293
 
292
294
  def _bind_service(self, client, port_name, address=None):
293
295
  self.ensure_one()
@@ -811,7 +813,7 @@ class SiiMixin(models.AbstractModel):
811
813
  ):
812
814
  doc_vals[
813
815
  "sii_account_registration_date"
814
- ] = self._get_account_registration_date()
816
+ ] = document._get_account_registration_date()
815
817
  doc_vals["sii_return"] = res
816
818
  send_error = False
817
819
  if res_line["CodigoErrorRegistro"]:
@@ -831,12 +833,12 @@ class SiiMixin(models.AbstractModel):
831
833
  "aeat_send_error": repr(fault)[:60],
832
834
  "sii_return": repr(fault),
833
835
  "aeat_content_sent": json.dumps(inv_dict, indent=4),
836
+ "sii_send_date": False,
834
837
  }
835
838
  )
836
839
  document.write(doc_vals)
837
840
  new_cr.commit()
838
841
  new_cr.close()
839
- raise ValidationError(fault) from fault
840
842
 
841
843
  def confirm_one_document(self):
842
844
  self.sudo()._send_document_to_sii()
@@ -0,0 +1,20 @@
1
+ Para configurar este módulo es necesario:
2
+
3
+ 1. En la compañia se almacenan las URLs del servicio SOAP de hacienda.
4
+ Estas URLs pueden cambiar según comunidades
5
+ 2. Los certificados deben alojarse en una carpeta accesible por la
6
+ instalación de Odoo.
7
+ 3. Preparar el certificado. El certificado enviado por la FMNT es en
8
+ formato p12, este certificado no se puede usar directamente con
9
+ Zeep. Se tiene que extraer la clave pública y la clave privada.
10
+
11
+ El parámetro del sistema l10n_es_aeat_sii_oca.sii_batch almacena el
12
+ número de facturas máximo que se enviarán al SII cada vez que se realice
13
+ un envío. El valor por defecto es 50.
14
+
15
+ En Linux se pueden usar los siguientes comandos:
16
+
17
+ - Clave pública: "openssl pkcs12 -in Certificado.p12 -nokeys -out
18
+ publicCert.crt -nodes"
19
+ - Clave privada: "openssl pkcs12 -in Certifcado.p12 -nocerts -out
20
+ privateKey.pem -nodes"
@@ -0,0 +1,24 @@
1
+ - Ignacio Ibeas \<<ignacio@acysos.com>\>
2
+ - Rubén Cerdà \<<ruben.cerda.roig@diagram.es>\>
3
+ - Ramon Guiu \<<ramon.guiu@minorisa.net>\>
4
+ - Pablo Fuentes \<<pablo@studio73.es>\>
5
+ - Jordi Tolsà \<<jordi@studio73.es>\>
6
+ - Ismael Calvo \<<ismael.calvo@factorlibre.es>\>
7
+ - Omar Castiñeira - Comunitea S.L. \<<omar@comunitea.com>\>
8
+ - Juanjo Algaz \<<jalgaz@gmail.com>\>, Planeta Huerto
9
+ \<<juanjoalgaz@planetahuerto.es>\>
10
+ - Javi Melendez \<<javimelex@gmail.com>\>
11
+ - Santi Argüeso - Comunitea S.L. \<<santi@comunitea.com>\>
12
+ - Angel Moya - PESOL \<<angel.moya@pesol.es>\>
13
+ - Eric Antonés - NuoBiT Solutions, S.L. \<<eantones@nuobit.com>\>
14
+ - [Sygel](https://www.sygel.es):
15
+ - Valentin Vinagre
16
+ - Manuel Regidor
17
+ - [Tecnativa](https://www.tecnativa.com):
18
+ - Pedro M. Baeza
19
+ - João Marques
20
+ - Lois Rilo Antelo \<<lois.rilo@forgeflow.com>\>
21
+ - Eduardo de Miguel (<edu@moduon.team>)
22
+ - Jose Zambudio \<<jose@aurestic.es>\>
23
+ - [Factor Libre](https://factorlibre.com):
24
+ - Luis J. Salvatierra \<<luis.salvatierra@factorlibre.com>\>
@@ -0,0 +1,2 @@
1
+ Módulo para la presentación inmediata del IVA
2
+ <https://www.agenciatributaria.es/static_files/AEAT/Contenidos_Comunes/La_Agencia_Tributaria/Modelos_y_formularios/Suministro_inmediato_informacion/FicherosSuministros/V_1_1/SII_Descripcion_ServicioWeb_v1.1.pdf>
@@ -0,0 +1,6 @@
1
+ Para instalar esté módulo necesita:
2
+
3
+ 1. Libreria Python Zeep, se puede instalar con el comando 'pip install
4
+ zeep'
5
+ 2. Libreria Python Requests, se puede instalar con el comando 'pip
6
+ install requests'
@@ -0,0 +1,12 @@
1
+ - Comunicación de cobros y pagos.
2
+ - Determinadas facturas intracomunitarias (Articulo 66 RIVA).
3
+ - Asistente para consultar los documentos comunicados.
4
+ - Libro de bienes de inversión (Libro anual se crea un módulo aparte).
5
+ - Regímenes especiales de seguros, de agencias de viaje o de bienes
6
+ usados.
7
+ - Devolución de IVA de viajeros.
8
+ - Facturas rectificativas por sustitución.
9
+ - Sistema de control de número de reintentos en caso de error en el envío.
10
+ - Soportar facturas de canje de facturas simplificadas por facturas
11
+ completas. Ver <https://github.com/OCA/l10n-spain/issues/1171> para
12
+ más información.
@@ -1,2 +1,2 @@
1
- Cuando se valida una factura automáticamente envia la comunicación al servidor
2
- de AEAT.
1
+ Cuando se valida una factura automáticamente envia la comunicación al
2
+ servidor de AEAT.
@@ -6,5 +6,4 @@ access_model_aeat_sii_map_lines_aeat,aeat.sii.map.lines aeat,model_aeat_sii_map_
6
6
  access_model_aeat_sii_mapping_registration_keys_admin,aeat.sii.mapping.registration.keys admin,model_aeat_sii_mapping_registration_keys,base.group_system,1,1,1,1
7
7
  access_model_aeat_sii_mapping_registration_keys_aeat,aeat.sii.mapping.registration.keys aeat,model_aeat_sii_mapping_registration_keys,l10n_es_aeat.group_account_aeat,1,0,0,0
8
8
  access_model_aeat_sii_mapping_registration_keys_aeat_account,aeat.sii.mapping.registration.keys aeat,model_aeat_sii_mapping_registration_keys,account.group_account_invoice,1,0,0,0
9
- access_queue_job,access_queue_job aeat,queue_job.model_queue_job,l10n_es_aeat.group_account_aeat,1,0,0,0
10
9
  access_wizard_send_sii,access_wizard_send_sii,model_wizard_send_sii,l10n_es_aeat.group_account_aeat,1,1,1,1