odoo-addon-contract 17.0.1.4.3.2__py3-none-any.whl → 18.0.2.0.8__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/contract/README.rst +14 -10
- odoo/addons/contract/__manifest__.py +3 -10
- odoo/addons/contract/controllers/main.py +1 -8
- odoo/addons/contract/data/contract_cron.xml +0 -2
- odoo/addons/contract/data/mail_template.xml +18 -17
- odoo/addons/contract/data/template_mail_notification.xml +1 -1
- odoo/addons/contract/i18n/am.po +146 -821
- odoo/addons/contract/i18n/ar.po +146 -821
- odoo/addons/contract/i18n/bg.po +146 -821
- odoo/addons/contract/i18n/bs.po +146 -821
- odoo/addons/contract/i18n/ca.po +835 -900
- odoo/addons/contract/i18n/ca_ES.po +146 -821
- odoo/addons/contract/i18n/contract.pot +145 -818
- odoo/addons/contract/i18n/cs.po +146 -821
- odoo/addons/contract/i18n/da.po +146 -821
- odoo/addons/contract/i18n/de.po +712 -953
- odoo/addons/contract/i18n/el_GR.po +146 -821
- odoo/addons/contract/i18n/en_GB.po +146 -821
- odoo/addons/contract/i18n/es.po +714 -947
- odoo/addons/contract/i18n/es_AR.po +550 -877
- odoo/addons/contract/i18n/es_CL.po +146 -821
- odoo/addons/contract/i18n/es_CO.po +146 -821
- odoo/addons/contract/i18n/es_CR.po +146 -821
- odoo/addons/contract/i18n/es_DO.po +146 -821
- odoo/addons/contract/i18n/es_EC.po +146 -821
- odoo/addons/contract/i18n/es_MX.po +146 -821
- odoo/addons/contract/i18n/es_PY.po +146 -821
- odoo/addons/contract/i18n/es_VE.po +146 -821
- odoo/addons/contract/i18n/et.po +146 -821
- odoo/addons/contract/i18n/eu.po +146 -821
- odoo/addons/contract/i18n/fa.po +146 -821
- odoo/addons/contract/i18n/fi.po +422 -848
- odoo/addons/contract/i18n/fr.po +713 -953
- odoo/addons/contract/i18n/fr_CA.po +146 -821
- odoo/addons/contract/i18n/fr_CH.po +146 -821
- odoo/addons/contract/i18n/fr_FR.po +454 -850
- odoo/addons/contract/i18n/gl.po +257 -846
- odoo/addons/contract/i18n/gl_ES.po +146 -821
- odoo/addons/contract/i18n/he.po +146 -821
- odoo/addons/contract/i18n/hi_IN.po +191 -831
- odoo/addons/contract/i18n/hr.po +211 -837
- odoo/addons/contract/i18n/hr_HR.po +223 -839
- odoo/addons/contract/i18n/hu.po +146 -821
- odoo/addons/contract/i18n/id.po +146 -821
- odoo/addons/contract/i18n/it.po +753 -902
- odoo/addons/contract/i18n/ja.po +146 -821
- odoo/addons/contract/i18n/ko.po +146 -821
- odoo/addons/contract/i18n/lt.po +146 -821
- odoo/addons/contract/i18n/lt_LT.po +146 -821
- odoo/addons/contract/i18n/lv.po +146 -821
- odoo/addons/contract/i18n/mk.po +146 -821
- odoo/addons/contract/i18n/mn.po +146 -821
- odoo/addons/contract/i18n/nb.po +146 -821
- odoo/addons/contract/i18n/nb_NO.po +146 -821
- odoo/addons/contract/i18n/nl.po +699 -953
- odoo/addons/contract/i18n/nl_BE.po +146 -821
- odoo/addons/contract/i18n/nl_NL.po +191 -831
- odoo/addons/contract/i18n/pl.po +146 -821
- odoo/addons/contract/i18n/pt.po +415 -839
- odoo/addons/contract/i18n/pt_BR.po +704 -947
- odoo/addons/contract/i18n/pt_PT.po +146 -821
- odoo/addons/contract/i18n/ro.po +146 -821
- odoo/addons/contract/i18n/ru.po +191 -831
- odoo/addons/contract/i18n/sk.po +146 -821
- odoo/addons/contract/i18n/sk_SK.po +146 -821
- odoo/addons/contract/i18n/sl.po +146 -821
- odoo/addons/contract/i18n/sr.po +146 -821
- odoo/addons/contract/i18n/sr@latin.po +146 -821
- odoo/addons/contract/i18n/sv.po +784 -933
- odoo/addons/contract/i18n/th.po +146 -821
- odoo/addons/contract/i18n/tr.po +611 -879
- odoo/addons/contract/i18n/tr_TR.po +221 -838
- odoo/addons/contract/i18n/uk.po +146 -821
- odoo/addons/contract/i18n/vi.po +146 -821
- odoo/addons/contract/i18n/vi_VN.po +146 -821
- odoo/addons/contract/i18n/zh_CN.po +407 -840
- odoo/addons/contract/i18n/zh_TW.po +150 -822
- odoo/addons/contract/migrations/18.0.2.0.0/end-migrate.py +27 -0
- odoo/addons/contract/migrations/18.0.2.0.0/pre-migrate.py +94 -0
- odoo/addons/contract/models/__init__.py +2 -6
- odoo/addons/contract/models/account_move.py +0 -8
- odoo/addons/contract/models/account_move_line.py +14 -0
- odoo/addons/contract/models/contract.py +272 -308
- odoo/addons/contract/models/contract_line.py +37 -859
- odoo/addons/contract/models/{contract_recurrency_mixin.py → contract_recurring_mixin.py} +101 -82
- odoo/addons/contract/models/contract_tag.py +1 -3
- odoo/addons/contract/models/contract_template.py +81 -2
- odoo/addons/contract/models/contract_template_line.py +250 -3
- odoo/addons/contract/report/contract_views.xml +0 -2
- odoo/addons/contract/report/report_contract.xml +13 -13
- odoo/addons/contract/security/contract_security.xml +6 -15
- odoo/addons/contract/security/contract_tag.xml +1 -3
- odoo/addons/contract/security/ir.model.access.csv +0 -2
- odoo/addons/contract/static/description/index.html +24 -18
- odoo/addons/contract/static/src/img/contract_icon.svg +4 -0
- odoo/addons/contract/static/src/js/contract_portal_tour.esm.js +6 -4
- odoo/addons/contract/tests/test_contract.py +82 -928
- odoo/addons/contract/tests/test_multicompany.py +5 -4
- odoo/addons/contract/tests/test_portal.py +6 -3
- odoo/addons/contract/views/contract.xml +92 -235
- odoo/addons/contract/views/contract_line.xml +48 -117
- odoo/addons/contract/views/contract_portal_templates.xml +187 -224
- odoo/addons/contract/views/contract_tag.xml +3 -3
- odoo/addons/contract/views/contract_template.xml +100 -72
- odoo/addons/contract/views/contract_template_line.xml +76 -5
- odoo/addons/contract/views/res_config_settings.xml +5 -6
- odoo/addons/contract/views/res_partner_view.xml +0 -5
- odoo/addons/contract/wizards/__init__.py +0 -2
- odoo/addons/contract/wizards/contract_manually_create_invoice.py +6 -6
- odoo/addons/contract/wizards/contract_manually_create_invoice.xml +2 -3
- {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/METADATA +18 -13
- odoo_addon_contract-18.0.2.0.8.dist-info/RECORD +132 -0
- {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/WHEEL +1 -1
- odoo/addons/contract/data/contract_renew_cron.xml +0 -14
- odoo/addons/contract/models/abstract_contract.py +0 -82
- odoo/addons/contract/models/abstract_contract_line.py +0 -271
- odoo/addons/contract/models/contract_line_constraints.py +0 -429
- odoo/addons/contract/models/contract_terminate_reason.py +0 -14
- odoo/addons/contract/models/res_company.py +0 -15
- odoo/addons/contract/models/res_config_settings.py +0 -18
- odoo/addons/contract/security/contract_terminate_reason.xml +0 -23
- odoo/addons/contract/security/groups.xml +0 -9
- odoo/addons/contract/views/abstract_contract_line.xml +0 -117
- odoo/addons/contract/views/contract_terminate_reason.xml +0 -38
- odoo/addons/contract/wizards/contract_contract_terminate.py +0 -42
- odoo/addons/contract/wizards/contract_contract_terminate.xml +0 -33
- odoo/addons/contract/wizards/contract_line_wizard.py +0 -53
- odoo/addons/contract/wizards/contract_line_wizard.xml +0 -111
- odoo_addon_contract-17.0.1.4.3.2.dist-info/RECORD +0 -143
- {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/top_level.txt +0 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
# Copyright 2018 ACSONE SA/NV
|
|
7
7
|
# Copyright 2021 Tecnativa - Víctor Martínez
|
|
8
8
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
9
|
+
|
|
9
10
|
import logging
|
|
10
11
|
|
|
11
12
|
from markupsafe import Markup
|
|
12
13
|
|
|
13
14
|
from odoo import Command, api, fields, models
|
|
14
|
-
from odoo.exceptions import
|
|
15
|
+
from odoo.exceptions import ValidationError
|
|
15
16
|
from odoo.osv import expression
|
|
16
|
-
from odoo.tools.translate import _
|
|
17
17
|
|
|
18
18
|
_logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
@@ -23,24 +23,52 @@ class ContractContract(models.Model):
|
|
|
23
23
|
_description = "Contract"
|
|
24
24
|
_order = "code, name asc"
|
|
25
25
|
_inherit = [
|
|
26
|
+
"contract.template",
|
|
27
|
+
"portal.mixin",
|
|
26
28
|
"mail.thread",
|
|
27
29
|
"mail.activity.mixin",
|
|
28
|
-
"contract.abstract.contract",
|
|
29
|
-
"contract.recurrency.mixin",
|
|
30
|
-
"portal.mixin",
|
|
31
30
|
]
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
# === Basic Information ===
|
|
33
|
+
active = fields.Boolean(default=True)
|
|
34
|
+
code = fields.Char(string="Reference")
|
|
35
|
+
name = fields.Char()
|
|
36
|
+
user_id = fields.Many2one(
|
|
37
|
+
comodel_name="res.users",
|
|
38
|
+
string="Responsible",
|
|
39
|
+
index=True,
|
|
40
|
+
default=lambda self: self.env.user,
|
|
38
41
|
)
|
|
39
42
|
group_id = fields.Many2one(
|
|
40
43
|
string="Group",
|
|
41
44
|
comodel_name="account.analytic.account",
|
|
42
45
|
ondelete="restrict",
|
|
43
46
|
)
|
|
47
|
+
tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
|
|
48
|
+
note = fields.Text(string="Notes")
|
|
49
|
+
|
|
50
|
+
# === Partner and Commercial Info ===
|
|
51
|
+
partner_id = fields.Many2one(
|
|
52
|
+
comodel_name="res.partner",
|
|
53
|
+
inverse="_inverse_partner_id",
|
|
54
|
+
required=True,
|
|
55
|
+
)
|
|
56
|
+
invoice_partner_id = fields.Many2one(
|
|
57
|
+
string="Invoicing contact",
|
|
58
|
+
comodel_name="res.partner",
|
|
59
|
+
ondelete="restrict",
|
|
60
|
+
domain="['|',('id', 'parent_of', partner_id), ('id', 'child_of', partner_id)]",
|
|
61
|
+
)
|
|
62
|
+
commercial_partner_id = fields.Many2one(
|
|
63
|
+
"res.partner",
|
|
64
|
+
compute_sudo=True,
|
|
65
|
+
related="partner_id.commercial_partner_id",
|
|
66
|
+
store=True,
|
|
67
|
+
string="Commercial Entity",
|
|
68
|
+
index=True,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# === Financial & Invoicing Info ===
|
|
44
72
|
currency_id = fields.Many2one(
|
|
45
73
|
compute="_compute_currency_id",
|
|
46
74
|
inverse="_inverse_currency_id",
|
|
@@ -51,8 +79,25 @@ class ContractContract(models.Model):
|
|
|
51
79
|
comodel_name="res.currency",
|
|
52
80
|
readonly=True,
|
|
53
81
|
)
|
|
82
|
+
payment_term_id = fields.Many2one(
|
|
83
|
+
comodel_name="account.payment.term",
|
|
84
|
+
string="Payment Terms",
|
|
85
|
+
index=True,
|
|
86
|
+
)
|
|
87
|
+
fiscal_position_id = fields.Many2one(
|
|
88
|
+
comodel_name="account.fiscal.position",
|
|
89
|
+
string="Fiscal Position",
|
|
90
|
+
ondelete="restrict",
|
|
91
|
+
)
|
|
92
|
+
invoice_count = fields.Integer(compute="_compute_invoice_count")
|
|
93
|
+
create_invoice_visibility = fields.Boolean(
|
|
94
|
+
compute="_compute_create_invoice_visibility"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# === Contract Template and Lines ===
|
|
54
98
|
contract_template_id = fields.Many2one(
|
|
55
|
-
string="Contract Template",
|
|
99
|
+
string="Contract Template",
|
|
100
|
+
comodel_name="contract.template",
|
|
56
101
|
)
|
|
57
102
|
contract_line_ids = fields.One2many(
|
|
58
103
|
string="Contract lines",
|
|
@@ -61,11 +106,6 @@ class ContractContract(models.Model):
|
|
|
61
106
|
copy=True,
|
|
62
107
|
context={"active_test": False},
|
|
63
108
|
)
|
|
64
|
-
# Trick for being able to have 2 different views for the same o2m
|
|
65
|
-
# We need this as one2many widget doesn't allow to define in the view
|
|
66
|
-
# the same field 2 times with different views. 2 views are needed because
|
|
67
|
-
# one of them must be editable inline and the other not, which can't be
|
|
68
|
-
# parametrized through attrs.
|
|
69
109
|
contract_line_fixed_ids = fields.One2many(
|
|
70
110
|
string="Contract lines (fixed)",
|
|
71
111
|
comodel_name="contract.line",
|
|
@@ -73,188 +113,22 @@ class ContractContract(models.Model):
|
|
|
73
113
|
context={"active_test": False},
|
|
74
114
|
)
|
|
75
115
|
|
|
76
|
-
|
|
77
|
-
comodel_name="res.users",
|
|
78
|
-
string="Responsible",
|
|
79
|
-
index=True,
|
|
80
|
-
default=lambda self: self.env.user,
|
|
81
|
-
)
|
|
82
|
-
create_invoice_visibility = fields.Boolean(
|
|
83
|
-
compute="_compute_create_invoice_visibility"
|
|
84
|
-
)
|
|
85
|
-
date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False)
|
|
86
|
-
payment_term_id = fields.Many2one(
|
|
87
|
-
comodel_name="account.payment.term", string="Payment Terms", index=True
|
|
88
|
-
)
|
|
89
|
-
invoice_count = fields.Integer(compute="_compute_invoice_count")
|
|
90
|
-
fiscal_position_id = fields.Many2one(
|
|
91
|
-
comodel_name="account.fiscal.position",
|
|
92
|
-
string="Fiscal Position",
|
|
93
|
-
ondelete="restrict",
|
|
94
|
-
)
|
|
95
|
-
invoice_partner_id = fields.Many2one(
|
|
96
|
-
string="Invoicing contact",
|
|
97
|
-
comodel_name="res.partner",
|
|
98
|
-
ondelete="restrict",
|
|
99
|
-
domain="['|',('id', 'parent_of', partner_id), ('id', 'child_of', partner_id)]",
|
|
100
|
-
)
|
|
101
|
-
partner_id = fields.Many2one(
|
|
102
|
-
comodel_name="res.partner", inverse="_inverse_partner_id", required=True
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
commercial_partner_id = fields.Many2one(
|
|
106
|
-
"res.partner",
|
|
107
|
-
compute_sudo=True,
|
|
108
|
-
related="partner_id.commercial_partner_id",
|
|
109
|
-
store=True,
|
|
110
|
-
string="Commercial Entity",
|
|
111
|
-
index=True,
|
|
112
|
-
)
|
|
113
|
-
tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
|
|
114
|
-
note = fields.Text(string="Notes")
|
|
115
|
-
is_terminated = fields.Boolean(string="Terminated", readonly=True, copy=False)
|
|
116
|
-
terminate_reason_id = fields.Many2one(
|
|
117
|
-
comodel_name="contract.terminate.reason",
|
|
118
|
-
string="Termination Reason",
|
|
119
|
-
ondelete="restrict",
|
|
120
|
-
readonly=True,
|
|
121
|
-
copy=False,
|
|
122
|
-
tracking=True,
|
|
123
|
-
)
|
|
124
|
-
terminate_comment = fields.Text(
|
|
125
|
-
string="Termination Comment",
|
|
126
|
-
readonly=True,
|
|
127
|
-
copy=False,
|
|
128
|
-
tracking=True,
|
|
129
|
-
)
|
|
130
|
-
terminate_date = fields.Date(
|
|
131
|
-
string="Termination Date",
|
|
132
|
-
readonly=True,
|
|
133
|
-
copy=False,
|
|
134
|
-
tracking=True,
|
|
135
|
-
)
|
|
116
|
+
# === Modification tracking ===
|
|
136
117
|
modification_ids = fields.One2many(
|
|
137
118
|
comodel_name="contract.modification",
|
|
138
119
|
inverse_name="contract_id",
|
|
139
120
|
string="Modifications",
|
|
140
121
|
)
|
|
141
122
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return self.env.ref("contract.contract_contract_customer_form_view").id
|
|
145
|
-
else:
|
|
146
|
-
return self.env.ref("contract.contract_contract_supplier_form_view").id
|
|
147
|
-
|
|
148
|
-
@api.model_create_multi
|
|
149
|
-
def create(self, vals_list):
|
|
150
|
-
records = super().create(vals_list)
|
|
151
|
-
records._set_start_contract_modification()
|
|
152
|
-
return records
|
|
153
|
-
|
|
154
|
-
def write(self, vals):
|
|
155
|
-
if "modification_ids" in vals:
|
|
156
|
-
res = super(
|
|
157
|
-
ContractContract, self.with_context(bypass_modification_send=True)
|
|
158
|
-
).write(vals)
|
|
159
|
-
self._modification_mail_send()
|
|
160
|
-
else:
|
|
161
|
-
res = super().write(vals)
|
|
162
|
-
return res
|
|
163
|
-
|
|
164
|
-
@api.model
|
|
165
|
-
def _set_start_contract_modification(self):
|
|
166
|
-
subtype_id = self.env.ref("contract.mail_message_subtype_contract_modification")
|
|
167
|
-
for record in self:
|
|
168
|
-
if record.contract_line_ids:
|
|
169
|
-
date_start = min(record.contract_line_ids.mapped("date_start"))
|
|
170
|
-
else:
|
|
171
|
-
date_start = record.create_date
|
|
172
|
-
record.message_subscribe(
|
|
173
|
-
partner_ids=[record.partner_id.id], subtype_ids=[subtype_id.id]
|
|
174
|
-
)
|
|
175
|
-
record.with_context(skip_modification_mail=True).write(
|
|
176
|
-
{
|
|
177
|
-
"modification_ids": [
|
|
178
|
-
(0, 0, {"date": date_start, "description": _("Contract start")})
|
|
179
|
-
]
|
|
180
|
-
}
|
|
181
|
-
)
|
|
123
|
+
# === Dates ===
|
|
124
|
+
date_end = fields.Date(compute="_compute_date_end", store=True, readonly=False)
|
|
182
125
|
|
|
183
|
-
|
|
184
|
-
def _modification_mail_send(self):
|
|
185
|
-
for record in self:
|
|
186
|
-
modification_ids_not_sent = record.modification_ids.filtered(
|
|
187
|
-
lambda x: not x.sent
|
|
188
|
-
)
|
|
189
|
-
if modification_ids_not_sent:
|
|
190
|
-
if not self.env.context.get("skip_modification_mail"):
|
|
191
|
-
subtype_id = self.env["ir.model.data"]._xmlid_to_res_id(
|
|
192
|
-
"contract.mail_message_subtype_contract_modification"
|
|
193
|
-
)
|
|
194
|
-
template_id = self.env.ref(
|
|
195
|
-
"contract.mail_template_contract_modification"
|
|
196
|
-
)
|
|
197
|
-
record.message_post_with_source(
|
|
198
|
-
template_id,
|
|
199
|
-
subtype_id=subtype_id,
|
|
200
|
-
)
|
|
201
|
-
modification_ids_not_sent.write({"sent": True})
|
|
126
|
+
# === Compute Methods ===
|
|
202
127
|
|
|
203
128
|
def _compute_access_url(self):
|
|
204
129
|
for record in self:
|
|
205
130
|
record.access_url = f"/my/contracts/{record.id}"
|
|
206
131
|
|
|
207
|
-
def action_preview(self):
|
|
208
|
-
"""Invoked when 'Preview' button in contract form view is clicked."""
|
|
209
|
-
self.ensure_one()
|
|
210
|
-
return {
|
|
211
|
-
"type": "ir.actions.act_url",
|
|
212
|
-
"target": "self",
|
|
213
|
-
"url": self.get_portal_url(),
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
def _inverse_partner_id(self):
|
|
217
|
-
for rec in self:
|
|
218
|
-
if not rec.invoice_partner_id:
|
|
219
|
-
rec.invoice_partner_id = rec.partner_id.address_get(["invoice"])[
|
|
220
|
-
"invoice"
|
|
221
|
-
]
|
|
222
|
-
|
|
223
|
-
def _get_related_invoices(self):
|
|
224
|
-
self.ensure_one()
|
|
225
|
-
|
|
226
|
-
invoices = (
|
|
227
|
-
self.env["account.move.line"]
|
|
228
|
-
.search(
|
|
229
|
-
[
|
|
230
|
-
(
|
|
231
|
-
"contract_line_id",
|
|
232
|
-
"in",
|
|
233
|
-
self.contract_line_ids.ids,
|
|
234
|
-
)
|
|
235
|
-
]
|
|
236
|
-
)
|
|
237
|
-
.mapped("move_id")
|
|
238
|
-
)
|
|
239
|
-
# we are forced to always search for this for not losing possible <=v11
|
|
240
|
-
# generated invoices
|
|
241
|
-
invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)])
|
|
242
|
-
return invoices
|
|
243
|
-
|
|
244
|
-
def _get_computed_currency(self):
|
|
245
|
-
"""Helper method for returning the theoretical computed currency."""
|
|
246
|
-
self.ensure_one()
|
|
247
|
-
currency = self.env["res.currency"]
|
|
248
|
-
if any(self.contract_line_ids.mapped("automatic_price")):
|
|
249
|
-
# Use pricelist currency
|
|
250
|
-
currency = (
|
|
251
|
-
self.pricelist_id.currency_id
|
|
252
|
-
or self.partner_id.with_company(
|
|
253
|
-
self.company_id
|
|
254
|
-
).property_product_pricelist.currency_id
|
|
255
|
-
)
|
|
256
|
-
return currency or self.journal_id.currency_id or self.company_id.currency_id
|
|
257
|
-
|
|
258
132
|
@api.depends(
|
|
259
133
|
"manual_currency_id",
|
|
260
134
|
"pricelist_id",
|
|
@@ -283,42 +157,16 @@ class ContractContract(models.Model):
|
|
|
283
157
|
for rec in self:
|
|
284
158
|
rec.invoice_count = len(rec._get_related_invoices())
|
|
285
159
|
|
|
286
|
-
def action_show_invoices(self):
|
|
287
|
-
self.ensure_one()
|
|
288
|
-
tree_view = self.env.ref("account.view_invoice_tree", raise_if_not_found=False)
|
|
289
|
-
form_view = self.env.ref("account.view_move_form", raise_if_not_found=False)
|
|
290
|
-
ctx = dict(self.env.context)
|
|
291
|
-
if ctx.get("default_contract_type"):
|
|
292
|
-
ctx["default_move_type"] = (
|
|
293
|
-
"out_invoice"
|
|
294
|
-
if ctx.get("default_contract_type") == "sale"
|
|
295
|
-
else "in_invoice"
|
|
296
|
-
)
|
|
297
|
-
action = {
|
|
298
|
-
"type": "ir.actions.act_window",
|
|
299
|
-
"name": "Invoices",
|
|
300
|
-
"res_model": "account.move",
|
|
301
|
-
"view_mode": "tree,kanban,form,calendar,pivot,graph,activity",
|
|
302
|
-
"domain": [("id", "in", self._get_related_invoices().ids)],
|
|
303
|
-
"context": ctx,
|
|
304
|
-
}
|
|
305
|
-
if tree_view and form_view:
|
|
306
|
-
action["views"] = [(tree_view.id, "tree"), (form_view.id, "form")]
|
|
307
|
-
return action
|
|
308
|
-
|
|
309
|
-
@api.depends("contract_line_ids.date_end")
|
|
310
|
-
def _compute_date_end(self):
|
|
311
|
-
for contract in self:
|
|
312
|
-
contract.date_end = False
|
|
313
|
-
date_end = contract.contract_line_ids.mapped("date_end")
|
|
314
|
-
if date_end and all(date_end):
|
|
315
|
-
contract.date_end = max(date_end)
|
|
316
|
-
|
|
317
160
|
@api.depends(
|
|
161
|
+
"next_period_date_start",
|
|
162
|
+
"recurring_invoicing_type",
|
|
163
|
+
"recurring_invoicing_offset",
|
|
164
|
+
"recurring_rule_type",
|
|
165
|
+
"recurring_interval",
|
|
166
|
+
"date_end",
|
|
318
167
|
"contract_line_ids.recurring_next_date",
|
|
319
168
|
"contract_line_ids.is_canceled",
|
|
320
169
|
)
|
|
321
|
-
# pylint: disable=missing-return
|
|
322
170
|
def _compute_recurring_next_date(self):
|
|
323
171
|
for contract in self:
|
|
324
172
|
recurring_next_date = contract.contract_line_ids.filtered(
|
|
@@ -334,7 +182,14 @@ class ContractContract(models.Model):
|
|
|
334
182
|
and contract._origin.date_start != contract.date_start
|
|
335
183
|
or not recurring_next_date
|
|
336
184
|
):
|
|
337
|
-
|
|
185
|
+
contract.recurring_next_date = self.get_next_invoice_date(
|
|
186
|
+
contract.next_period_date_start,
|
|
187
|
+
contract.recurring_invoicing_type,
|
|
188
|
+
contract.recurring_invoicing_offset,
|
|
189
|
+
contract.recurring_rule_type,
|
|
190
|
+
contract.recurring_interval,
|
|
191
|
+
max_date_end=contract.date_end,
|
|
192
|
+
)
|
|
338
193
|
else:
|
|
339
194
|
contract.recurring_next_date = min(recurring_next_date)
|
|
340
195
|
|
|
@@ -345,6 +200,23 @@ class ContractContract(models.Model):
|
|
|
345
200
|
contract.contract_line_ids.mapped("create_invoice_visibility")
|
|
346
201
|
)
|
|
347
202
|
|
|
203
|
+
@api.depends("contract_line_ids.date_end")
|
|
204
|
+
def _compute_date_end(self):
|
|
205
|
+
for contract in self:
|
|
206
|
+
contract.date_end = False
|
|
207
|
+
date_end = contract.contract_line_ids.mapped("date_end")
|
|
208
|
+
if date_end and all(date_end):
|
|
209
|
+
contract.date_end = max(date_end)
|
|
210
|
+
|
|
211
|
+
def _inverse_partner_id(self):
|
|
212
|
+
for rec in self:
|
|
213
|
+
if not rec.invoice_partner_id:
|
|
214
|
+
rec.invoice_partner_id = rec.partner_id.address_get(["invoice"])[
|
|
215
|
+
"invoice"
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
# === Onchange Methods ===
|
|
219
|
+
|
|
348
220
|
@api.onchange("contract_template_id")
|
|
349
221
|
def _onchange_contract_template_id(self):
|
|
350
222
|
"""Update the contract fields with that of the template.
|
|
@@ -371,7 +243,10 @@ class ContractContract(models.Model):
|
|
|
371
243
|
field.name in self.NO_SYNC,
|
|
372
244
|
)
|
|
373
245
|
):
|
|
374
|
-
if
|
|
246
|
+
if (
|
|
247
|
+
self.contract_template_id[field_name]
|
|
248
|
+
and self.contract_template_id[field_name] != self[field_name]
|
|
249
|
+
):
|
|
375
250
|
self[field_name] = self.contract_template_id[field_name]
|
|
376
251
|
|
|
377
252
|
@api.onchange("partner_id", "company_id")
|
|
@@ -391,6 +266,180 @@ class ContractContract(models.Model):
|
|
|
391
266
|
self.payment_term_id = partner.property_payment_term_id
|
|
392
267
|
self.invoice_partner_id = self.partner_id.address_get(["invoice"])["invoice"]
|
|
393
268
|
|
|
269
|
+
# === CRUD ===
|
|
270
|
+
@api.model_create_multi
|
|
271
|
+
def create(self, vals_list):
|
|
272
|
+
records = super().create(vals_list)
|
|
273
|
+
records._set_start_contract_modification()
|
|
274
|
+
return records
|
|
275
|
+
|
|
276
|
+
def write(self, vals):
|
|
277
|
+
if "modification_ids" in vals:
|
|
278
|
+
res = super(
|
|
279
|
+
ContractContract, self.with_context(bypass_modification_send=True)
|
|
280
|
+
).write(vals)
|
|
281
|
+
self._modification_mail_send()
|
|
282
|
+
else:
|
|
283
|
+
res = super().write(vals)
|
|
284
|
+
return res
|
|
285
|
+
|
|
286
|
+
# === Actions ===
|
|
287
|
+
|
|
288
|
+
def action_preview(self):
|
|
289
|
+
"""Invoked when 'Preview' button in contract form view is clicked."""
|
|
290
|
+
self.ensure_one()
|
|
291
|
+
return {
|
|
292
|
+
"type": "ir.actions.act_url",
|
|
293
|
+
"target": "self",
|
|
294
|
+
"url": self.get_portal_url(),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def action_show_invoices(self):
|
|
298
|
+
self.ensure_one()
|
|
299
|
+
tree_view = self.env.ref("account.view_invoice_tree", raise_if_not_found=False)
|
|
300
|
+
form_view = self.env.ref("account.view_move_form", raise_if_not_found=False)
|
|
301
|
+
ctx = dict(self.env.context)
|
|
302
|
+
if ctx.get("default_contract_type"):
|
|
303
|
+
ctx["default_move_type"] = (
|
|
304
|
+
"out_invoice"
|
|
305
|
+
if ctx.get("default_contract_type") == "sale"
|
|
306
|
+
else "in_invoice"
|
|
307
|
+
)
|
|
308
|
+
action = {
|
|
309
|
+
"type": "ir.actions.act_window",
|
|
310
|
+
"name": "Invoices",
|
|
311
|
+
"res_model": "account.move",
|
|
312
|
+
"view_mode": "list,kanban,form,calendar,pivot,graph,activity",
|
|
313
|
+
"domain": [("id", "in", self._get_related_invoices().ids)],
|
|
314
|
+
"context": ctx,
|
|
315
|
+
}
|
|
316
|
+
if tree_view and form_view:
|
|
317
|
+
action["views"] = [(tree_view.id, "list"), (form_view.id, "form")]
|
|
318
|
+
return action
|
|
319
|
+
|
|
320
|
+
def action_contract_send(self):
|
|
321
|
+
self.ensure_one()
|
|
322
|
+
template = self.env.ref("contract.email_contract_template", False)
|
|
323
|
+
compose_form = self.env.ref("mail.email_compose_message_wizard_form")
|
|
324
|
+
ctx = dict(
|
|
325
|
+
default_model="contract.contract",
|
|
326
|
+
default_res_ids=self.ids,
|
|
327
|
+
default_use_template=bool(template),
|
|
328
|
+
default_template_id=template and template.id or False,
|
|
329
|
+
default_composition_mode="comment",
|
|
330
|
+
)
|
|
331
|
+
return {
|
|
332
|
+
"name": self.env._("Compose Email"),
|
|
333
|
+
"type": "ir.actions.act_window",
|
|
334
|
+
"view_mode": "form",
|
|
335
|
+
"res_model": "mail.compose.message",
|
|
336
|
+
"views": [(compose_form.id, "form")],
|
|
337
|
+
"view_id": compose_form.id,
|
|
338
|
+
"target": "new",
|
|
339
|
+
"context": ctx,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
def recurring_create_invoice(self):
|
|
343
|
+
"""
|
|
344
|
+
Button action
|
|
345
|
+
This method triggers the creation of the next invoices of the contracts
|
|
346
|
+
even if their next invoicing date is in the future.
|
|
347
|
+
"""
|
|
348
|
+
self.ensure_one()
|
|
349
|
+
invoices = self._recurring_create_invoice()
|
|
350
|
+
for invoice in invoices:
|
|
351
|
+
body = Markup(
|
|
352
|
+
self.env._("Contract manually invoiced: %(invoice_link)s")
|
|
353
|
+
) % {"invoice_link": invoice._get_html_link(title=invoice.name)}
|
|
354
|
+
self.message_post(body=body)
|
|
355
|
+
return invoices
|
|
356
|
+
|
|
357
|
+
# === Helpers and Utilities ===
|
|
358
|
+
|
|
359
|
+
def get_formview_id(self, access_uid=None):
|
|
360
|
+
if self.contract_type == "sale":
|
|
361
|
+
return self.env.ref("contract.contract_contract_customer_form_view").id
|
|
362
|
+
else:
|
|
363
|
+
return self.env.ref("contract.contract_contract_supplier_form_view").id
|
|
364
|
+
|
|
365
|
+
def _set_start_contract_modification(self):
|
|
366
|
+
subtype_id = self.env.ref("contract.mail_message_subtype_contract_modification")
|
|
367
|
+
for record in self:
|
|
368
|
+
if record.contract_line_ids:
|
|
369
|
+
date_start = min(record.contract_line_ids.mapped("date_start"))
|
|
370
|
+
else:
|
|
371
|
+
date_start = record.create_date
|
|
372
|
+
record.message_subscribe(
|
|
373
|
+
partner_ids=[record.partner_id.id], subtype_ids=[subtype_id.id]
|
|
374
|
+
)
|
|
375
|
+
record.with_context(skip_modification_mail=True).write(
|
|
376
|
+
{
|
|
377
|
+
"modification_ids": [
|
|
378
|
+
Command.create(
|
|
379
|
+
{
|
|
380
|
+
"date": date_start,
|
|
381
|
+
"description": self.env._("Contract start"),
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def _modification_mail_send(self):
|
|
389
|
+
for record in self:
|
|
390
|
+
modification_ids_not_sent = record.modification_ids.filtered(
|
|
391
|
+
lambda x: not x.sent
|
|
392
|
+
)
|
|
393
|
+
if modification_ids_not_sent:
|
|
394
|
+
if not self.env.context.get("skip_modification_mail"):
|
|
395
|
+
subtype_id = self.env["ir.model.data"]._xmlid_to_res_id(
|
|
396
|
+
"contract.mail_message_subtype_contract_modification"
|
|
397
|
+
)
|
|
398
|
+
template_id = self.env.ref(
|
|
399
|
+
"contract.mail_template_contract_modification"
|
|
400
|
+
)
|
|
401
|
+
record.message_post_with_source(
|
|
402
|
+
template_id,
|
|
403
|
+
subtype_id=subtype_id,
|
|
404
|
+
email_layout_xmlid="contract.template_contract_modification",
|
|
405
|
+
)
|
|
406
|
+
modification_ids_not_sent.write({"sent": True})
|
|
407
|
+
|
|
408
|
+
def _get_related_invoices(self):
|
|
409
|
+
self.ensure_one()
|
|
410
|
+
|
|
411
|
+
invoices = (
|
|
412
|
+
self.env["account.move.line"]
|
|
413
|
+
.search(
|
|
414
|
+
[
|
|
415
|
+
(
|
|
416
|
+
"contract_line_id",
|
|
417
|
+
"in",
|
|
418
|
+
self.contract_line_ids.ids,
|
|
419
|
+
)
|
|
420
|
+
]
|
|
421
|
+
)
|
|
422
|
+
.mapped("move_id")
|
|
423
|
+
)
|
|
424
|
+
# we are forced to always search for this for not losing possible <=v11
|
|
425
|
+
# generated invoices
|
|
426
|
+
invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)])
|
|
427
|
+
return invoices
|
|
428
|
+
|
|
429
|
+
def _get_computed_currency(self):
|
|
430
|
+
"""Helper method for returning the theoretical computed currency."""
|
|
431
|
+
self.ensure_one()
|
|
432
|
+
currency = self.env["res.currency"]
|
|
433
|
+
if any(self.contract_line_ids.mapped("automatic_price")):
|
|
434
|
+
# Use pricelist currency
|
|
435
|
+
currency = (
|
|
436
|
+
self.pricelist_id.currency_id
|
|
437
|
+
or self.partner_id.with_company(
|
|
438
|
+
self.company_id
|
|
439
|
+
).property_product_pricelist.currency_id
|
|
440
|
+
)
|
|
441
|
+
return currency or self.journal_id.currency_id or self.company_id.currency_id
|
|
442
|
+
|
|
394
443
|
def _convert_contract_lines(self, contract):
|
|
395
444
|
self.ensure_one()
|
|
396
445
|
new_lines = self.env["contract.line"]
|
|
@@ -402,7 +451,6 @@ class ContractContract(models.Model):
|
|
|
402
451
|
vals["date_start"] = fields.Date.context_today(contract_line)
|
|
403
452
|
vals["recurring_next_date"] = fields.Date.context_today(contract_line)
|
|
404
453
|
new_lines += contract_line_model.new(vals)
|
|
405
|
-
new_lines._onchange_is_auto_renew()
|
|
406
454
|
return new_lines
|
|
407
455
|
|
|
408
456
|
def _prepare_invoice(self, date_invoice, journal=None):
|
|
@@ -425,7 +473,7 @@ class ContractContract(models.Model):
|
|
|
425
473
|
)
|
|
426
474
|
if not journal:
|
|
427
475
|
raise ValidationError(
|
|
428
|
-
_(
|
|
476
|
+
self.env._(
|
|
429
477
|
"Please define a %(contract_type)s journal "
|
|
430
478
|
"for the company '%(company)s'."
|
|
431
479
|
)
|
|
@@ -468,28 +516,6 @@ class ContractContract(models.Model):
|
|
|
468
516
|
)
|
|
469
517
|
return vals
|
|
470
518
|
|
|
471
|
-
def action_contract_send(self):
|
|
472
|
-
self.ensure_one()
|
|
473
|
-
template = self.env.ref("contract.email_contract_template", False)
|
|
474
|
-
compose_form = self.env.ref("mail.email_compose_message_wizard_form")
|
|
475
|
-
ctx = dict(
|
|
476
|
-
default_model="contract.contract",
|
|
477
|
-
default_res_ids=self.ids,
|
|
478
|
-
default_use_template=bool(template),
|
|
479
|
-
default_template_id=template and template.id or False,
|
|
480
|
-
default_composition_mode="comment",
|
|
481
|
-
)
|
|
482
|
-
return {
|
|
483
|
-
"name": _("Compose Email"),
|
|
484
|
-
"type": "ir.actions.act_window",
|
|
485
|
-
"view_mode": "form",
|
|
486
|
-
"res_model": "mail.compose.message",
|
|
487
|
-
"views": [(compose_form.id, "form")],
|
|
488
|
-
"view_id": compose_form.id,
|
|
489
|
-
"target": "new",
|
|
490
|
-
"context": ctx,
|
|
491
|
-
}
|
|
492
|
-
|
|
493
519
|
@api.model
|
|
494
520
|
def _get_contracts_to_invoice_domain(self, date_ref=None):
|
|
495
521
|
"""
|
|
@@ -579,22 +605,9 @@ class ContractContract(models.Model):
|
|
|
579
605
|
)
|
|
580
606
|
invoices_values.append(invoice_vals)
|
|
581
607
|
# Force the recomputation of journal items
|
|
582
|
-
contract_lines.
|
|
608
|
+
contract_lines._update_last_date_invoiced()
|
|
583
609
|
return invoices_values
|
|
584
610
|
|
|
585
|
-
def recurring_create_invoice(self):
|
|
586
|
-
"""
|
|
587
|
-
This method triggers the creation of the next invoices of the contracts
|
|
588
|
-
even if their next invoicing date is in the future.
|
|
589
|
-
"""
|
|
590
|
-
invoices = self._recurring_create_invoice()
|
|
591
|
-
for invoice in invoices:
|
|
592
|
-
body = Markup(_("Contract manually invoiced: %(invoice_link)s")) % {
|
|
593
|
-
"invoice_link": invoice._get_html_link(title=invoice.name)
|
|
594
|
-
}
|
|
595
|
-
self.message_post(body=body)
|
|
596
|
-
return invoices
|
|
597
|
-
|
|
598
611
|
@api.model
|
|
599
612
|
def _invoice_followers(self, invoices):
|
|
600
613
|
invoice_create_subtype = self.env.ref(
|
|
@@ -613,7 +626,7 @@ class ContractContract(models.Model):
|
|
|
613
626
|
def _add_contract_origin(self, invoices):
|
|
614
627
|
for item in self:
|
|
615
628
|
for move in invoices & item._get_related_invoices():
|
|
616
|
-
translation = _("by contract")
|
|
629
|
+
translation = self.env._("by contract")
|
|
617
630
|
move.message_post(
|
|
618
631
|
body=Markup(
|
|
619
632
|
f"{move._creation_message()} {translation} "
|
|
@@ -673,52 +686,3 @@ class ContractContract(models.Model):
|
|
|
673
686
|
@api.model
|
|
674
687
|
def cron_recurring_create_invoice(self, date_ref=None):
|
|
675
688
|
return self._cron_recurring_create(date_ref, create_type="invoice")
|
|
676
|
-
|
|
677
|
-
def action_terminate_contract(self):
|
|
678
|
-
self.ensure_one()
|
|
679
|
-
context = {"default_contract_id": self.id}
|
|
680
|
-
return {
|
|
681
|
-
"type": "ir.actions.act_window",
|
|
682
|
-
"name": _("Terminate Contract"),
|
|
683
|
-
"res_model": "contract.contract.terminate",
|
|
684
|
-
"view_mode": "form",
|
|
685
|
-
"target": "new",
|
|
686
|
-
"context": context,
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
def _terminate_contract(
|
|
690
|
-
self,
|
|
691
|
-
terminate_reason_id,
|
|
692
|
-
terminate_comment,
|
|
693
|
-
terminate_date,
|
|
694
|
-
terminate_lines_with_last_date_invoiced=False,
|
|
695
|
-
):
|
|
696
|
-
self.ensure_one()
|
|
697
|
-
if not self.env.user.has_group("contract.can_terminate_contract"):
|
|
698
|
-
raise UserError(_("You are not allowed to terminate contracts."))
|
|
699
|
-
for line in self.contract_line_ids.filtered("is_stop_allowed"):
|
|
700
|
-
line.stop(
|
|
701
|
-
max(terminate_date, line.last_date_invoiced)
|
|
702
|
-
if terminate_lines_with_last_date_invoiced and line.last_date_invoiced
|
|
703
|
-
else terminate_date
|
|
704
|
-
)
|
|
705
|
-
self.write(
|
|
706
|
-
{
|
|
707
|
-
"is_terminated": True,
|
|
708
|
-
"terminate_reason_id": terminate_reason_id.id,
|
|
709
|
-
"terminate_comment": terminate_comment,
|
|
710
|
-
"terminate_date": terminate_date,
|
|
711
|
-
}
|
|
712
|
-
)
|
|
713
|
-
return True
|
|
714
|
-
|
|
715
|
-
def action_cancel_contract_termination(self):
|
|
716
|
-
self.ensure_one()
|
|
717
|
-
self.write(
|
|
718
|
-
{
|
|
719
|
-
"is_terminated": False,
|
|
720
|
-
"terminate_reason_id": False,
|
|
721
|
-
"terminate_comment": False,
|
|
722
|
-
"terminate_date": False,
|
|
723
|
-
}
|
|
724
|
-
)
|