odoo-addon-l10n-es-verifactu-oca 18.0.1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/l10n_es_verifactu_oca/README.rst +195 -0
- odoo/addons/l10n_es_verifactu_oca/__init__.py +3 -0
- odoo/addons/l10n_es_verifactu_oca/__manifest__.py +50 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_config_parameter.xml +9 -0
- odoo/addons/l10n_es_verifactu_oca/data/ir_cron.xml +13 -0
- odoo/addons/l10n_es_verifactu_oca/data/l10n.es.aeat.map.tax.line.tax.csv +50 -0
- odoo/addons/l10n_es_verifactu_oca/data/mail_activity_data.xml +11 -0
- odoo/addons/l10n_es_verifactu_oca/data/neutralize.sql +2 -0
- odoo/addons/l10n_es_verifactu_oca/data/template/account.fiscal.position-es_common.csv +27 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.csv +2 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu.map.line.csv +8 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_registration_key_data.xml +205 -0
- odoo/addons/l10n_es_verifactu_oca/data/verifactu_tax_agency_data.xml +19 -0
- odoo/addons/l10n_es_verifactu_oca/hooks.py +43 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/es.po +1682 -0
- odoo/addons/l10n_es_verifactu_oca/i18n/l10n_es_verifactu_oca.pot +1640 -0
- odoo/addons/l10n_es_verifactu_oca/migrations/18.0.1.1.0/pre-migration.py +25 -0
- odoo/addons/l10n_es_verifactu_oca/models/__init__.py +15 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_chart_template.py +17 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_fiscal_position.py +34 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_journal.py +64 -0
- odoo/addons/l10n_es_verifactu_oca/models/account_move.py +631 -0
- odoo/addons/l10n_es_verifactu_oca/models/aeat_tax_agency.py +30 -0
- odoo/addons/l10n_es_verifactu_oca/models/res_company.py +48 -0
- odoo/addons/l10n_es_verifactu_oca/models/res_partner.py +26 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_chaining.py +37 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_developer.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry.py +398 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response.py +116 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_invoice_entry_response_line.py +47 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_map.py +68 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_mixin.py +485 -0
- odoo/addons/l10n_es_verifactu_oca/models/verifactu_registration_key.py +26 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONFIGURE.md +27 -0
- odoo/addons/l10n_es_verifactu_oca/readme/CONTRIBUTORS.md +18 -0
- odoo/addons/l10n_es_verifactu_oca/readme/DESCRIPTION.md +1 -0
- odoo/addons/l10n_es_verifactu_oca/readme/INSTALL.md +6 -0
- odoo/addons/l10n_es_verifactu_oca/readme/ROADMAP.md +30 -0
- odoo/addons/l10n_es_verifactu_oca/readme/USAGE.md +3 -0
- odoo/addons/l10n_es_verifactu_oca/security/ir.model.access.csv +23 -0
- odoo/addons/l10n_es_verifactu_oca/security/verifactu_security.xml +6 -0
- odoo/addons/l10n_es_verifactu_oca/static/description/icon.png +0 -0
- odoo/addons/l10n_es_verifactu_oca/static/description/index.html +551 -0
- odoo/addons/l10n_es_verifactu_oca/tests/__init__.py +2 -0
- odoo/addons/l10n_es_verifactu_oca/tests/common.py +281 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_accepted_with_errors.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_incorrect.json +37 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_cancel_with_errors.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_correct.json +35 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_duplicated.json +43 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_mocked_response_incorrect.json +37 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva10b_s_iva21s_dict.json +59 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_invoice_s_iva21s_s_req52_dict.json +58 -0
- odoo/addons/l10n_es_verifactu_oca/tests/json/verifactu_out_refund_s_iva10b_s_iva10b_s_iva21s_dict.json +66 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_10n_es_verifactu.py +506 -0
- odoo/addons/l10n_es_verifactu_oca/tests/test_verifactu_invoice.py +348 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_fiscal_position_view.xml +29 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_journal_view.xml +22 -0
- odoo/addons/l10n_es_verifactu_oca/views/account_move_view.xml +237 -0
- odoo/addons/l10n_es_verifactu_oca/views/aeat_tax_agency_view.xml +31 -0
- odoo/addons/l10n_es_verifactu_oca/views/report_invoice.xml +53 -0
- odoo/addons/l10n_es_verifactu_oca/views/res_company_view.xml +50 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_chaining_view.xml +45 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_developer_view.xml +46 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_response_view.xml +134 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_invoice_entry_view.xml +127 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_lines_view.xml +16 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_map_view.xml +54 -0
- odoo/addons/l10n_es_verifactu_oca/views/verifactu_registration_keys_view.xml +43 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/__init__.py +2 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/account_move_reversal.py +16 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard.py +24 -0
- odoo/addons/l10n_es_verifactu_oca/wizards/verifactu_cancel_invoice_wizard_view.xml +35 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/METADATA +213 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/RECORD +78 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/WHEEL +5 -0
- odoo_addon_l10n_es_verifactu_oca-18.0.1.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2024 Aures TIC - Jose Zambudio
|
|
2
|
+
# Copyright 2025 Tecnativa - Pedro M. Baeza
|
|
3
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
4
|
+
|
|
5
|
+
from odoo import fields, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResCompany(models.Model):
|
|
9
|
+
_inherit = "res.company"
|
|
10
|
+
|
|
11
|
+
verifactu_enabled = fields.Boolean(string="VERI*FACTU enabled", tracking=True)
|
|
12
|
+
verifactu_test = fields.Boolean(
|
|
13
|
+
string="VERI*FACTU test environment?", tracking=True
|
|
14
|
+
)
|
|
15
|
+
verifactu_description = fields.Text(
|
|
16
|
+
string="VERI*FACTU description",
|
|
17
|
+
default="/",
|
|
18
|
+
help="The description for VERI*FACTU invoices if not set",
|
|
19
|
+
tracking=True,
|
|
20
|
+
)
|
|
21
|
+
verifactu_developer_id = fields.Many2one(
|
|
22
|
+
comodel_name="verifactu.developer",
|
|
23
|
+
string="VERI*FACTU developer",
|
|
24
|
+
ondelete="restrict",
|
|
25
|
+
tracking=True,
|
|
26
|
+
)
|
|
27
|
+
verifactu_start_date = fields.Date(
|
|
28
|
+
string="VERI*FACTU start date",
|
|
29
|
+
help="If this field is set, the VERI*FACTU won't be enabled on invoices with "
|
|
30
|
+
"lower invoice date. If not set, it can be enabled on all invoice dates",
|
|
31
|
+
tracking=True,
|
|
32
|
+
)
|
|
33
|
+
verifactu_chaining_id = fields.Many2one(
|
|
34
|
+
comodel_name="verifactu.chaining",
|
|
35
|
+
string="VERI*FACTU chaining",
|
|
36
|
+
ondelete="restrict",
|
|
37
|
+
tracking=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def write(self, vals):
|
|
41
|
+
# As the compute is not triggered automatically, we need to manually trigger it
|
|
42
|
+
# rewriting the flag at journal level.
|
|
43
|
+
res = super().write(vals)
|
|
44
|
+
if vals.get("verifactu_enabled"):
|
|
45
|
+
self.env["account.journal"].search(
|
|
46
|
+
[("company_id", "in", self.ids), ("type", "=", "sale")]
|
|
47
|
+
).verifactu_enabled = True
|
|
48
|
+
return res
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright 2024 Aures TIC - Jose Zambudio
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
3
|
+
|
|
4
|
+
from odoo import api, fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResPartner(models.Model):
|
|
8
|
+
_inherit = "res.partner"
|
|
9
|
+
|
|
10
|
+
verifactu_enabled = fields.Boolean(
|
|
11
|
+
compute="_compute_aeat_sending_enabled", string="VERI*FACTU enabled"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
@api.depends("company_id")
|
|
15
|
+
def _compute_aeat_sending_enabled(self):
|
|
16
|
+
res = super()._compute_aeat_sending_enabled()
|
|
17
|
+
verifactu_enabled = any(self.env.companies.mapped("verifactu_enabled"))
|
|
18
|
+
for partner in self:
|
|
19
|
+
partner.verifactu_enabled = (
|
|
20
|
+
partner.company_id.verifactu_enabled
|
|
21
|
+
if partner.company_id
|
|
22
|
+
else verifactu_enabled
|
|
23
|
+
)
|
|
24
|
+
if partner.verifactu_enabled:
|
|
25
|
+
partner.aeat_sending_enabled = True
|
|
26
|
+
return res
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2024 Aures TIC - Almudena de La Puente <almudena@aurestic.es>
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VerifactuChaining(models.Model):
|
|
8
|
+
_name = "verifactu.chaining"
|
|
9
|
+
_inherit = "mail.thread"
|
|
10
|
+
_description = "VERI*FACTU chaining"
|
|
11
|
+
|
|
12
|
+
name = fields.Char(required=True, tracking=True)
|
|
13
|
+
last_verifactu_invoice_entry_id = fields.Many2one(
|
|
14
|
+
comodel_name="verifactu.invoice.entry",
|
|
15
|
+
string="Last invoice entry",
|
|
16
|
+
help="Reference to the last VERI*FACTU invoice entry for this company. "
|
|
17
|
+
"Used for atomic chaining.",
|
|
18
|
+
copy=False,
|
|
19
|
+
readonly=True,
|
|
20
|
+
)
|
|
21
|
+
sif_id = fields.Char(
|
|
22
|
+
string="SIF ID",
|
|
23
|
+
required=True,
|
|
24
|
+
tracking=True,
|
|
25
|
+
size=2,
|
|
26
|
+
help="Identifier of the billing software (SIF). "
|
|
27
|
+
"Must be exactly 2 alphanumeric characters (A,Z, 0,9).",
|
|
28
|
+
)
|
|
29
|
+
installation_number = fields.Integer(default=1, required=True, tracking=True)
|
|
30
|
+
|
|
31
|
+
_sql_constraints = [
|
|
32
|
+
(
|
|
33
|
+
"verifactu_chaining_name_uniq",
|
|
34
|
+
"unique(name)",
|
|
35
|
+
"A chaining with the same name already exists!",
|
|
36
|
+
)
|
|
37
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright 2024 Aures TIC - Almudena de La Puente
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VerifactuDeveloper(models.Model):
|
|
8
|
+
_name = "verifactu.developer"
|
|
9
|
+
_description = "VERI*FACTU developer"
|
|
10
|
+
_inherit = "mail.thread"
|
|
11
|
+
|
|
12
|
+
name = fields.Char(string="Developer Name", required=True, tracking=True)
|
|
13
|
+
vat = fields.Char(string="Developer VAT", required=True, tracking=True)
|
|
14
|
+
sif_name = fields.Char("SIF Name", required=True, tracking=True)
|
|
15
|
+
version = fields.Char(default="1.0", required=True, tracking=True)
|
|
16
|
+
responsibility_declaration = fields.Binary(attachment=True, copy=False)
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Copyright 2025 ForgeFlow S.L.
|
|
2
|
+
# Copyright 2025 Tecnativa - Pedro M. Baeza
|
|
3
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from requests import Session
|
|
8
|
+
from zeep import Client, Settings
|
|
9
|
+
from zeep.plugins import HistoryPlugin
|
|
10
|
+
from zeep.transports import Transport
|
|
11
|
+
|
|
12
|
+
from odoo import _, api, fields, models
|
|
13
|
+
from odoo.exceptions import UserError
|
|
14
|
+
from odoo.tools import split_every
|
|
15
|
+
|
|
16
|
+
VERIFACTU_SEND_STATES = [
|
|
17
|
+
("not_sent", "Not sent"),
|
|
18
|
+
("sent", "Sent and Correct"),
|
|
19
|
+
("incorrect", "Sent and Incorrect"),
|
|
20
|
+
("sent_w_errors", "Sent and accepted with errors"),
|
|
21
|
+
("cancel", "Cancelled "),
|
|
22
|
+
("cancel_w_errors", "Cancelled with Errors"),
|
|
23
|
+
("cancel_incorrect", "Incorrect cancellation"),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
VERIFACTU_STATE_MAPPING = {
|
|
27
|
+
"Correcto": "sent",
|
|
28
|
+
"Incorrecto": "incorrect",
|
|
29
|
+
"AceptadoConErrores": "sent_w_errors",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
VERIFACTU_CANCEL_STATE_MAPPING = {
|
|
33
|
+
"Correcto": "cancel",
|
|
34
|
+
"Incorrecto": "cancel_incorrect",
|
|
35
|
+
"AceptadoConErrores": "cancel_w_errors",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VerifactuInvoiceEntry(models.Model):
|
|
40
|
+
_name = "verifactu.invoice.entry"
|
|
41
|
+
_description = "VERI*FACTU invoice entry"
|
|
42
|
+
_order = "id desc"
|
|
43
|
+
_rec_name = "document_hash"
|
|
44
|
+
|
|
45
|
+
verifactu_chaining_id = fields.Many2one(
|
|
46
|
+
"verifactu.chaining", string="Chaining", ondelete="restrict", required=True
|
|
47
|
+
)
|
|
48
|
+
model = fields.Char(readonly=True, required=True)
|
|
49
|
+
document_id = fields.Many2oneReference(
|
|
50
|
+
string="Document", model_field="model", readonly=True, index=True, required=True
|
|
51
|
+
)
|
|
52
|
+
document_name = fields.Char(readonly=True)
|
|
53
|
+
previous_invoice_entry_id = fields.Many2one(
|
|
54
|
+
"verifactu.invoice.entry", string="Previous Invoice Entry", readonly=True
|
|
55
|
+
)
|
|
56
|
+
company_id = fields.Many2one(
|
|
57
|
+
"res.company", string="Company", required=True, readonly=True
|
|
58
|
+
)
|
|
59
|
+
document_hash = fields.Char(required=True, readonly=True)
|
|
60
|
+
aeat_json_data = fields.Text(
|
|
61
|
+
string="AEAT JSON Data",
|
|
62
|
+
help="Generated JSON data to send to AEAT",
|
|
63
|
+
readonly=True,
|
|
64
|
+
)
|
|
65
|
+
send_state = fields.Selection(
|
|
66
|
+
selection=VERIFACTU_SEND_STATES,
|
|
67
|
+
compute="_compute_send_state",
|
|
68
|
+
default="not_sent",
|
|
69
|
+
readonly=True,
|
|
70
|
+
store=True,
|
|
71
|
+
copy=False,
|
|
72
|
+
help="Indicates the state of this document in relation with the "
|
|
73
|
+
"presentation to VERI*FACTU.",
|
|
74
|
+
)
|
|
75
|
+
send_attempt = fields.Integer(
|
|
76
|
+
default=0, help="Number of attempts to send this document."
|
|
77
|
+
)
|
|
78
|
+
response_line_ids = fields.One2many(
|
|
79
|
+
"verifactu.invoice.entry.response.line",
|
|
80
|
+
"entry_id",
|
|
81
|
+
string="Responses",
|
|
82
|
+
help="Responses from VERI*FACTU after sending the documents.",
|
|
83
|
+
)
|
|
84
|
+
last_error_code = fields.Char(compute="_compute_last_error_code", store=True)
|
|
85
|
+
previous_hash = fields.Char(
|
|
86
|
+
related="previous_invoice_entry_id.document_hash",
|
|
87
|
+
readonly=True,
|
|
88
|
+
string="Previous Hash",
|
|
89
|
+
)
|
|
90
|
+
entry_type = fields.Selection(
|
|
91
|
+
selection=[
|
|
92
|
+
("register", "Register"),
|
|
93
|
+
("modify", "Modify"),
|
|
94
|
+
("cancel", "Cancel"),
|
|
95
|
+
],
|
|
96
|
+
default="register",
|
|
97
|
+
required=True,
|
|
98
|
+
)
|
|
99
|
+
last_response_line_id = fields.Many2one(
|
|
100
|
+
"verifactu.invoice.entry.response.line",
|
|
101
|
+
string="Last Response Line",
|
|
102
|
+
readonly=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@api.depends("response_line_ids", "response_line_ids.send_state")
|
|
106
|
+
def _compute_send_state(self):
|
|
107
|
+
for rec in self:
|
|
108
|
+
rec.send_state = "not_sent"
|
|
109
|
+
last_response = rec.last_response_line_id
|
|
110
|
+
if last_response:
|
|
111
|
+
rec.send_state = last_response.send_state
|
|
112
|
+
|
|
113
|
+
@api.depends("response_line_ids", "response_line_ids.error_code")
|
|
114
|
+
def _compute_last_error_code(self):
|
|
115
|
+
"""Compute the last error code from the response lines."""
|
|
116
|
+
for rec in self:
|
|
117
|
+
if rec.last_response_line_id:
|
|
118
|
+
rec.last_error_code = rec.last_response_line_id.error_code
|
|
119
|
+
else:
|
|
120
|
+
rec.last_error_code = ""
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def document(self):
|
|
124
|
+
return self.env[self.model].browse(self.document_id).exists()
|
|
125
|
+
|
|
126
|
+
@api.model
|
|
127
|
+
def _cron_send_documents_to_verifactu(self):
|
|
128
|
+
batch_limit = self.env["verifactu.mixin"]._get_verifactu_batch()
|
|
129
|
+
for chaining in self.env["verifactu.chaining"].search([]):
|
|
130
|
+
self.env.cr.execute(
|
|
131
|
+
"""
|
|
132
|
+
SELECT id FROM verifactu_invoice_entry AS vsq
|
|
133
|
+
WHERE vsq.send_state = 'not_sent'
|
|
134
|
+
AND vsq.verifactu_chaining_id = %s
|
|
135
|
+
ORDER BY id
|
|
136
|
+
FOR UPDATE NOWAIT
|
|
137
|
+
""",
|
|
138
|
+
[chaining.id],
|
|
139
|
+
)
|
|
140
|
+
entries_to_send_ids = [entry[0] for entry in self.env.cr.fetchall()]
|
|
141
|
+
for entries_batch_ids in split_every(batch_limit, entries_to_send_ids):
|
|
142
|
+
records_to_send = self.browse(entries_batch_ids)
|
|
143
|
+
send_date = fields.Datetime.now()
|
|
144
|
+
threshold_time = send_date - datetime.timedelta(seconds=240)
|
|
145
|
+
# Look for documents where we have to send as an incident
|
|
146
|
+
outdated_records = records_to_send.filtered(
|
|
147
|
+
lambda r, t=threshold_time: r.document.verifactu_registration_date
|
|
148
|
+
< t
|
|
149
|
+
)
|
|
150
|
+
current_records = records_to_send - outdated_records
|
|
151
|
+
outdated_records.with_context(
|
|
152
|
+
verifactu_incident=True
|
|
153
|
+
)._send_documents_to_verifactu()
|
|
154
|
+
current_records._send_documents_to_verifactu()
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
def _get_verifactu_aeat_header(self):
|
|
158
|
+
"""Builds VERI*FACTU send header
|
|
159
|
+
|
|
160
|
+
:param tipo_comunicacion String 'A0': new reg, 'A1': modification
|
|
161
|
+
:param cancellation Bool True when the communitacion es for document
|
|
162
|
+
cancellation
|
|
163
|
+
:return Dict with header data depending on cancellation
|
|
164
|
+
"""
|
|
165
|
+
self.ensure_one()
|
|
166
|
+
if not self.company_id.vat:
|
|
167
|
+
raise UserError(
|
|
168
|
+
_("No VAT configured for the company '{}'").format(self.company_id.name)
|
|
169
|
+
)
|
|
170
|
+
header = {
|
|
171
|
+
"ObligadoEmision": {
|
|
172
|
+
"NombreRazon": self.company_id.name[0:120],
|
|
173
|
+
"NIF": self.company_id.partner_id._parse_aeat_vat_info()[2],
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
incident = self.env.context.get("verifactu_incident", False)
|
|
177
|
+
if incident:
|
|
178
|
+
header.update({"RemisionVoluntaria": {"Incidencia": "S"}})
|
|
179
|
+
return header
|
|
180
|
+
|
|
181
|
+
def _bind_verifactu_service(self, client, port_name, address=None):
|
|
182
|
+
self.ensure_one()
|
|
183
|
+
service = client._get_service("sfVerifactu")
|
|
184
|
+
port = client._get_port(service, port_name)
|
|
185
|
+
address = address or port.binding_options["address"]
|
|
186
|
+
return client.create_service(port.binding.name, address)
|
|
187
|
+
|
|
188
|
+
def _connect_verifactu_params_aeat(self):
|
|
189
|
+
self.ensure_one()
|
|
190
|
+
agency = self.company_id.tax_agency_id
|
|
191
|
+
if not agency:
|
|
192
|
+
# We use spanish agency by default to keep old behavior with
|
|
193
|
+
# ir.config parameters. In the future it might be good to reinforce
|
|
194
|
+
# to explicitly set a tax agency in the company by raising an error
|
|
195
|
+
# here.
|
|
196
|
+
agency = self.env.ref("l10n_es_aeat.aeat_tax_agency_spain")
|
|
197
|
+
return agency._connect_params_verifactu(self.company_id)
|
|
198
|
+
|
|
199
|
+
def _connect_verifactu(self):
|
|
200
|
+
self.ensure_one()
|
|
201
|
+
public_crt, private_key = self.env["l10n.es.aeat.certificate"].get_certificates(
|
|
202
|
+
company=self.company_id
|
|
203
|
+
)
|
|
204
|
+
if not public_crt or not private_key:
|
|
205
|
+
raise UserError(
|
|
206
|
+
_("Please, configure the VERI*FACTU certificates for your company")
|
|
207
|
+
)
|
|
208
|
+
params = self._connect_verifactu_params_aeat()
|
|
209
|
+
session = Session()
|
|
210
|
+
session.cert = (public_crt, private_key)
|
|
211
|
+
transport = Transport(session=session)
|
|
212
|
+
history = HistoryPlugin()
|
|
213
|
+
settings = Settings(forbid_entities=False)
|
|
214
|
+
client = Client(
|
|
215
|
+
wsdl=params["wsdl"],
|
|
216
|
+
transport=transport,
|
|
217
|
+
plugins=[history],
|
|
218
|
+
settings=settings,
|
|
219
|
+
)
|
|
220
|
+
return self._bind_verifactu_service(
|
|
221
|
+
client, params["port_name"], params["address"]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def _process_response_line_doc_vals(
|
|
225
|
+
self,
|
|
226
|
+
verifactu_response=False,
|
|
227
|
+
verifactu_response_line=False,
|
|
228
|
+
response_line=False,
|
|
229
|
+
previous_response_line=False,
|
|
230
|
+
header_sent=False,
|
|
231
|
+
):
|
|
232
|
+
estado_registro = verifactu_response_line["EstadoRegistro"]
|
|
233
|
+
doc_vals = {
|
|
234
|
+
"aeat_header_sent": json.dumps(header_sent, indent=4),
|
|
235
|
+
}
|
|
236
|
+
doc_vals["verifactu_return"] = verifactu_response_line
|
|
237
|
+
send_error = False
|
|
238
|
+
if hasattr(verifactu_response_line, "CodigoErrorRegistro"):
|
|
239
|
+
send_error = "{} | {}".format(
|
|
240
|
+
str(verifactu_response_line["CodigoErrorRegistro"]),
|
|
241
|
+
str(verifactu_response_line["DescripcionErrorRegistro"]),
|
|
242
|
+
)
|
|
243
|
+
# si ya ha devuelto previamente registro duplicado, parseamos el estado
|
|
244
|
+
# del registro duplicado para dejar la factura correcta o incorrecta
|
|
245
|
+
if (
|
|
246
|
+
verifactu_response_line["CodigoErrorRegistro"] == 3000
|
|
247
|
+
and previous_response_line
|
|
248
|
+
):
|
|
249
|
+
registroDuplicado = verifactu_response_line["RegistroDuplicado"]
|
|
250
|
+
estado_registro = registroDuplicado["EstadoRegistroDuplicado"]
|
|
251
|
+
# en duplicados devuelve Correcta en vez de Correcto...
|
|
252
|
+
if estado_registro == "Correcta":
|
|
253
|
+
estado_registro = "Correcto"
|
|
254
|
+
elif registroDuplicado["CodigoErrorRegistro"]:
|
|
255
|
+
# en duplicados devuelve AceptadaConErrores en vez de
|
|
256
|
+
# AceptadoConErrores...
|
|
257
|
+
if estado_registro == "AceptadaConErrores":
|
|
258
|
+
estado_registro = "AceptadoConErrores"
|
|
259
|
+
send_error = "{} | {}".format(
|
|
260
|
+
str(registroDuplicado["CodigoErrorRegistro"]),
|
|
261
|
+
str(registroDuplicado["DescripcionErrorRegistro"]),
|
|
262
|
+
)
|
|
263
|
+
response_line.send_state = VERIFACTU_STATE_MAPPING[estado_registro]
|
|
264
|
+
if response_line.is_cancellation:
|
|
265
|
+
response_line.send_state = VERIFACTU_CANCEL_STATE_MAPPING[
|
|
266
|
+
estado_registro
|
|
267
|
+
]
|
|
268
|
+
if estado_registro == "Correcto":
|
|
269
|
+
doc_vals.update(
|
|
270
|
+
{
|
|
271
|
+
"verifactu_csv": verifactu_response["CSV"],
|
|
272
|
+
"aeat_send_failed": False,
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
elif estado_registro == "AceptadoConErrores":
|
|
276
|
+
doc_vals.update(
|
|
277
|
+
{
|
|
278
|
+
"verifactu_csv": verifactu_response["CSV"],
|
|
279
|
+
"aeat_send_failed": True,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
doc_vals["aeat_send_failed"] = True
|
|
284
|
+
doc_vals["aeat_send_error"] = send_error
|
|
285
|
+
if response_line.document_id:
|
|
286
|
+
response_line.document.write(doc_vals)
|
|
287
|
+
return doc_vals
|
|
288
|
+
|
|
289
|
+
def _send_documents_to_verifactu(self):
|
|
290
|
+
if not self:
|
|
291
|
+
return False
|
|
292
|
+
rec = self[0]
|
|
293
|
+
header = rec._get_verifactu_aeat_header()
|
|
294
|
+
registro_factura_list = []
|
|
295
|
+
create_exception = False
|
|
296
|
+
for rec in self:
|
|
297
|
+
rec.send_attempt += 1
|
|
298
|
+
if rec.document:
|
|
299
|
+
inv_dict = rec.document._get_verifactu_invoice_dict(
|
|
300
|
+
cancel=rec.entry_type == "cancel"
|
|
301
|
+
)
|
|
302
|
+
registro_factura_list.append(inv_dict)
|
|
303
|
+
try:
|
|
304
|
+
serv = rec._connect_verifactu()
|
|
305
|
+
res = serv.RegFactuSistemaFacturacion(header, registro_factura_list)
|
|
306
|
+
except Exception:
|
|
307
|
+
res = {}
|
|
308
|
+
create_exception = True
|
|
309
|
+
response_name = ""
|
|
310
|
+
response = (
|
|
311
|
+
self.env["verifactu.invoice.entry.response"]
|
|
312
|
+
.sudo()
|
|
313
|
+
.create(
|
|
314
|
+
{
|
|
315
|
+
"header": json.dumps(header),
|
|
316
|
+
"name": response_name,
|
|
317
|
+
"invoice_data": json.dumps(registro_factura_list),
|
|
318
|
+
"response": res,
|
|
319
|
+
"verifactu_csv": "CSV" in res and res["CSV"] or _("-"),
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
response.complete_open_activity_on_exception()
|
|
324
|
+
if create_exception:
|
|
325
|
+
if not response.date_response:
|
|
326
|
+
response.date_response = fields.Datetime.now()
|
|
327
|
+
response.create_activity_on_exception()
|
|
328
|
+
create_response_activity = self._create_response_lines(
|
|
329
|
+
response=response, header=header, verifactu_response=res
|
|
330
|
+
)
|
|
331
|
+
updated_response_name = _("VERI*FACTU sending")
|
|
332
|
+
if create_exception:
|
|
333
|
+
updated_response_name = _("Connection error with VERI*FACTU")
|
|
334
|
+
elif create_response_activity:
|
|
335
|
+
updated_response_name = _("Incorrect invoices sent to VERI*FACTU")
|
|
336
|
+
response.name = updated_response_name
|
|
337
|
+
if create_response_activity:
|
|
338
|
+
response.create_send_response_activity()
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
def _create_response_lines(
|
|
342
|
+
self, response=False, header=False, verifactu_response=False
|
|
343
|
+
):
|
|
344
|
+
create_response_activity = False
|
|
345
|
+
# the returned object doesn't have `get` method, so use this form
|
|
346
|
+
verifactu_response_lines = (
|
|
347
|
+
"RespuestaLinea" in verifactu_response
|
|
348
|
+
and verifactu_response["RespuestaLinea"]
|
|
349
|
+
or []
|
|
350
|
+
)
|
|
351
|
+
models = self.env["verifactu.mixin"]._get_verifactu_reference_models()
|
|
352
|
+
for verifactu_response_line in verifactu_response_lines:
|
|
353
|
+
invoice_num = verifactu_response_line["IDFactura"]["NumSerieFactura"]
|
|
354
|
+
for model in models:
|
|
355
|
+
if document := self.env[model].search(
|
|
356
|
+
[
|
|
357
|
+
("name", "=", invoice_num),
|
|
358
|
+
("id", "in", self.mapped("document_id")),
|
|
359
|
+
],
|
|
360
|
+
limit=1,
|
|
361
|
+
):
|
|
362
|
+
break
|
|
363
|
+
# Find the verifactu.invoice entry for this document
|
|
364
|
+
verifactu_invoice_entry = document.last_verifactu_invoice_entry_id
|
|
365
|
+
previous_response_line = document.last_verifactu_response_line_id
|
|
366
|
+
send_state = VERIFACTU_STATE_MAPPING[
|
|
367
|
+
verifactu_response_line["EstadoRegistro"]
|
|
368
|
+
]
|
|
369
|
+
if verifactu_invoice_entry.entry_type == "cancel":
|
|
370
|
+
send_state = VERIFACTU_CANCEL_STATE_MAPPING[
|
|
371
|
+
verifactu_response_line["EstadoRegistro"]
|
|
372
|
+
]
|
|
373
|
+
vals = {
|
|
374
|
+
"entry_id": verifactu_invoice_entry.id,
|
|
375
|
+
"model": verifactu_invoice_entry.model,
|
|
376
|
+
"document_id": verifactu_invoice_entry.document_id,
|
|
377
|
+
"response": verifactu_response_line,
|
|
378
|
+
"entry_response_id": response.id,
|
|
379
|
+
"send_state": send_state,
|
|
380
|
+
"error_code": "CodigoErrorRegistro" in verifactu_response_line
|
|
381
|
+
and str(verifactu_response_line["CodigoErrorRegistro"])
|
|
382
|
+
or "",
|
|
383
|
+
}
|
|
384
|
+
response_line = (
|
|
385
|
+
self.env["verifactu.invoice.entry.response.line"].sudo().create(vals)
|
|
386
|
+
)
|
|
387
|
+
document.last_verifactu_response_line_id = response_line
|
|
388
|
+
verifactu_invoice_entry.last_response_line_id = response_line
|
|
389
|
+
self._process_response_line_doc_vals(
|
|
390
|
+
verifactu_response=verifactu_response,
|
|
391
|
+
verifactu_response_line=verifactu_response_line,
|
|
392
|
+
response_line=response_line,
|
|
393
|
+
previous_response_line=previous_response_line,
|
|
394
|
+
header_sent=header,
|
|
395
|
+
)
|
|
396
|
+
if send_state not in ("sent", "cancel"):
|
|
397
|
+
create_response_activity = True
|
|
398
|
+
return create_response_activity
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Copyright 2025 ForgeFlow S.L.
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
3
|
+
from odoo import _, fields, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VerifactuInvoiceEntryResponse(models.Model):
|
|
7
|
+
_name = "verifactu.invoice.entry.response"
|
|
8
|
+
_description = "VERI*FACTU Send Response"
|
|
9
|
+
_inherit = ["mail.activity.mixin", "mail.thread"]
|
|
10
|
+
_order = "id desc"
|
|
11
|
+
|
|
12
|
+
header = fields.Text()
|
|
13
|
+
name = fields.Char()
|
|
14
|
+
invoice_data = fields.Text()
|
|
15
|
+
response = fields.Text()
|
|
16
|
+
verifactu_csv = fields.Text(string="VERI*FACTU CSV")
|
|
17
|
+
date_response = fields.Datetime(readonly=True)
|
|
18
|
+
activity_type_id = fields.Many2one(
|
|
19
|
+
"mail.activity.type",
|
|
20
|
+
string="Activity Type",
|
|
21
|
+
compute="_compute_activity_type_id",
|
|
22
|
+
store=True,
|
|
23
|
+
)
|
|
24
|
+
response_line_ids = fields.One2many(
|
|
25
|
+
"verifactu.invoice.entry.response.line",
|
|
26
|
+
"entry_response_id",
|
|
27
|
+
string="Response lines",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def _compute_activity_type_id(self):
|
|
31
|
+
for record in self:
|
|
32
|
+
activity = self.env["mail.activity"].search(
|
|
33
|
+
[
|
|
34
|
+
("res_model", "=", "verifactu.invoice.entry.response"),
|
|
35
|
+
("res_id", "=", record.id),
|
|
36
|
+
],
|
|
37
|
+
limit=1,
|
|
38
|
+
)
|
|
39
|
+
record.activity_type_id = activity.activity_type_id if activity else False
|
|
40
|
+
|
|
41
|
+
def create_activity_on_exception(self):
|
|
42
|
+
model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
|
|
43
|
+
exception_activity_type = self.env.ref(
|
|
44
|
+
"l10n_es_verifactu_oca.mail_activity_data_exception"
|
|
45
|
+
)
|
|
46
|
+
activity_vals = []
|
|
47
|
+
responsible_group = self.env.ref(
|
|
48
|
+
"l10n_es_verifactu_oca.group_verifactu_responsible"
|
|
49
|
+
)
|
|
50
|
+
users = responsible_group.users
|
|
51
|
+
for record in self:
|
|
52
|
+
existing = self.env["mail.activity"].search_count(
|
|
53
|
+
[
|
|
54
|
+
("activity_type_id", "=", exception_activity_type.id),
|
|
55
|
+
("res_model", "=", "verifactu.invoice.entry.response"),
|
|
56
|
+
],
|
|
57
|
+
limit=1,
|
|
58
|
+
)
|
|
59
|
+
if not existing:
|
|
60
|
+
user = users[:1] or self.env.user
|
|
61
|
+
activity_vals.append(
|
|
62
|
+
{
|
|
63
|
+
"res_model_id": model_id,
|
|
64
|
+
"res_model": "verifactu.invoice.entry.response",
|
|
65
|
+
"res_id": record.id,
|
|
66
|
+
"activity_type_id": exception_activity_type.id,
|
|
67
|
+
"user_id": user.id,
|
|
68
|
+
"summary": _("Check connection error with VERI*FACTU"),
|
|
69
|
+
"note": _(
|
|
70
|
+
"There has been an error when trying to connect to "
|
|
71
|
+
"VERI*FACTU"
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
if activity_vals:
|
|
76
|
+
return self.env["mail.activity"].create(activity_vals)
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def create_send_response_activity(self):
|
|
80
|
+
activity_type = self.env.ref("mail.mail_activity_data_warning")
|
|
81
|
+
model_id = self.env["ir.model"]._get_id("verifactu.invoice.entry.response")
|
|
82
|
+
activity_vals = []
|
|
83
|
+
responsible_group = self.env.ref(
|
|
84
|
+
"l10n_es_verifactu_oca.group_verifactu_responsible"
|
|
85
|
+
)
|
|
86
|
+
users = responsible_group.users
|
|
87
|
+
for record in self:
|
|
88
|
+
user = users[:1] or self.env.user
|
|
89
|
+
activity_vals.append(
|
|
90
|
+
{
|
|
91
|
+
"activity_type_id": activity_type.id,
|
|
92
|
+
"user_id": user.id,
|
|
93
|
+
"res_id": record.id,
|
|
94
|
+
"res_model": "verifactu.invoice.entry.response",
|
|
95
|
+
"res_model_id": model_id,
|
|
96
|
+
"summary": _("Check incorrect invoices from VERI*FACTU"),
|
|
97
|
+
"note": _("There is an error with one or more invoices"),
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
return self.env["mail.activity"].create(activity_vals)
|
|
101
|
+
|
|
102
|
+
def complete_open_activity_on_exception(self):
|
|
103
|
+
exception_activity_type = self.env.ref(
|
|
104
|
+
"l10n_es_verifactu_oca.mail_activity_data_exception"
|
|
105
|
+
)
|
|
106
|
+
for _record in self:
|
|
107
|
+
activity = self.env["mail.activity"].search(
|
|
108
|
+
[
|
|
109
|
+
("activity_type_id", "=", exception_activity_type.id),
|
|
110
|
+
("res_model", "=", "verifactu.invoice.entry.response"),
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
for act in activity:
|
|
114
|
+
if act.state != "done":
|
|
115
|
+
act.action_done()
|
|
116
|
+
return True
|