odoo-addon-contract 17.0.1.4.5.1__py3-none-any.whl → 18.0.2.0.0.9__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/contract.pot +141 -823
- odoo/addons/contract/migrations/18.0.2.0.0/pre-migrate.py +92 -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 +266 -308
- odoo/addons/contract/models/contract_line.py +34 -861
- 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 +249 -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/js/contract_portal_tour.esm.js +6 -3
- odoo/addons/contract/tests/test_contract.py +42 -927
- odoo/addons/contract/tests/test_multicompany.py +5 -4
- odoo/addons/contract/tests/test_portal.py +6 -3
- odoo/addons/contract/views/contract.xml +91 -234
- odoo/addons/contract/views/contract_line.xml +48 -117
- odoo/addons/contract/views/contract_portal_templates.xml +181 -222
- 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.5.1.dist-info → odoo_addon_contract-18.0.2.0.0.9.dist-info}/METADATA +17 -13
- {odoo_addon_contract-17.0.1.4.5.1.dist-info → odoo_addon_contract-18.0.2.0.0.9.dist-info}/RECORD +42 -55
- 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.5.1.dist-info → odoo_addon_contract-18.0.2.0.0.9.dist-info}/WHEEL +0 -0
- {odoo_addon_contract-17.0.1.4.5.1.dist-info → odoo_addon_contract-18.0.2.0.0.9.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,189 +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
|
-
email_layout_xmlid="contract.template_contract_modification",
|
|
201
|
-
)
|
|
202
|
-
modification_ids_not_sent.write({"sent": True})
|
|
126
|
+
# === Compute Methods ===
|
|
203
127
|
|
|
204
128
|
def _compute_access_url(self):
|
|
205
129
|
for record in self:
|
|
206
130
|
record.access_url = f"/my/contracts/{record.id}"
|
|
207
131
|
|
|
208
|
-
def action_preview(self):
|
|
209
|
-
"""Invoked when 'Preview' button in contract form view is clicked."""
|
|
210
|
-
self.ensure_one()
|
|
211
|
-
return {
|
|
212
|
-
"type": "ir.actions.act_url",
|
|
213
|
-
"target": "self",
|
|
214
|
-
"url": self.get_portal_url(),
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
def _inverse_partner_id(self):
|
|
218
|
-
for rec in self:
|
|
219
|
-
if not rec.invoice_partner_id:
|
|
220
|
-
rec.invoice_partner_id = rec.partner_id.address_get(["invoice"])[
|
|
221
|
-
"invoice"
|
|
222
|
-
]
|
|
223
|
-
|
|
224
|
-
def _get_related_invoices(self):
|
|
225
|
-
self.ensure_one()
|
|
226
|
-
|
|
227
|
-
invoices = (
|
|
228
|
-
self.env["account.move.line"]
|
|
229
|
-
.search(
|
|
230
|
-
[
|
|
231
|
-
(
|
|
232
|
-
"contract_line_id",
|
|
233
|
-
"in",
|
|
234
|
-
self.contract_line_ids.ids,
|
|
235
|
-
)
|
|
236
|
-
]
|
|
237
|
-
)
|
|
238
|
-
.mapped("move_id")
|
|
239
|
-
)
|
|
240
|
-
# we are forced to always search for this for not losing possible <=v11
|
|
241
|
-
# generated invoices
|
|
242
|
-
invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)])
|
|
243
|
-
return invoices
|
|
244
|
-
|
|
245
|
-
def _get_computed_currency(self):
|
|
246
|
-
"""Helper method for returning the theoretical computed currency."""
|
|
247
|
-
self.ensure_one()
|
|
248
|
-
currency = self.env["res.currency"]
|
|
249
|
-
if any(self.contract_line_ids.mapped("automatic_price")):
|
|
250
|
-
# Use pricelist currency
|
|
251
|
-
currency = (
|
|
252
|
-
self.pricelist_id.currency_id
|
|
253
|
-
or self.partner_id.with_company(
|
|
254
|
-
self.company_id
|
|
255
|
-
).property_product_pricelist.currency_id
|
|
256
|
-
)
|
|
257
|
-
return currency or self.journal_id.currency_id or self.company_id.currency_id
|
|
258
|
-
|
|
259
132
|
@api.depends(
|
|
260
133
|
"manual_currency_id",
|
|
261
134
|
"pricelist_id",
|
|
@@ -284,42 +157,16 @@ class ContractContract(models.Model):
|
|
|
284
157
|
for rec in self:
|
|
285
158
|
rec.invoice_count = len(rec._get_related_invoices())
|
|
286
159
|
|
|
287
|
-
def action_show_invoices(self):
|
|
288
|
-
self.ensure_one()
|
|
289
|
-
tree_view = self.env.ref("account.view_invoice_tree", raise_if_not_found=False)
|
|
290
|
-
form_view = self.env.ref("account.view_move_form", raise_if_not_found=False)
|
|
291
|
-
ctx = dict(self.env.context)
|
|
292
|
-
if ctx.get("default_contract_type"):
|
|
293
|
-
ctx["default_move_type"] = (
|
|
294
|
-
"out_invoice"
|
|
295
|
-
if ctx.get("default_contract_type") == "sale"
|
|
296
|
-
else "in_invoice"
|
|
297
|
-
)
|
|
298
|
-
action = {
|
|
299
|
-
"type": "ir.actions.act_window",
|
|
300
|
-
"name": "Invoices",
|
|
301
|
-
"res_model": "account.move",
|
|
302
|
-
"view_mode": "tree,kanban,form,calendar,pivot,graph,activity",
|
|
303
|
-
"domain": [("id", "in", self._get_related_invoices().ids)],
|
|
304
|
-
"context": ctx,
|
|
305
|
-
}
|
|
306
|
-
if tree_view and form_view:
|
|
307
|
-
action["views"] = [(tree_view.id, "tree"), (form_view.id, "form")]
|
|
308
|
-
return action
|
|
309
|
-
|
|
310
|
-
@api.depends("contract_line_ids.date_end")
|
|
311
|
-
def _compute_date_end(self):
|
|
312
|
-
for contract in self:
|
|
313
|
-
contract.date_end = False
|
|
314
|
-
date_end = contract.contract_line_ids.mapped("date_end")
|
|
315
|
-
if date_end and all(date_end):
|
|
316
|
-
contract.date_end = max(date_end)
|
|
317
|
-
|
|
318
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",
|
|
319
167
|
"contract_line_ids.recurring_next_date",
|
|
320
168
|
"contract_line_ids.is_canceled",
|
|
321
169
|
)
|
|
322
|
-
# pylint: disable=missing-return
|
|
323
170
|
def _compute_recurring_next_date(self):
|
|
324
171
|
for contract in self:
|
|
325
172
|
recurring_next_date = contract.contract_line_ids.filtered(
|
|
@@ -335,7 +182,14 @@ class ContractContract(models.Model):
|
|
|
335
182
|
and contract._origin.date_start != contract.date_start
|
|
336
183
|
or not recurring_next_date
|
|
337
184
|
):
|
|
338
|
-
|
|
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
|
+
)
|
|
339
193
|
else:
|
|
340
194
|
contract.recurring_next_date = min(recurring_next_date)
|
|
341
195
|
|
|
@@ -346,6 +200,23 @@ class ContractContract(models.Model):
|
|
|
346
200
|
contract.contract_line_ids.mapped("create_invoice_visibility")
|
|
347
201
|
)
|
|
348
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
|
+
|
|
349
220
|
@api.onchange("contract_template_id")
|
|
350
221
|
def _onchange_contract_template_id(self):
|
|
351
222
|
"""Update the contract fields with that of the template.
|
|
@@ -392,6 +263,178 @@ class ContractContract(models.Model):
|
|
|
392
263
|
self.payment_term_id = partner.property_payment_term_id
|
|
393
264
|
self.invoice_partner_id = self.partner_id.address_get(["invoice"])["invoice"]
|
|
394
265
|
|
|
266
|
+
# === CRUD ===
|
|
267
|
+
@api.model_create_multi
|
|
268
|
+
def create(self, vals_list):
|
|
269
|
+
records = super().create(vals_list)
|
|
270
|
+
records._set_start_contract_modification()
|
|
271
|
+
return records
|
|
272
|
+
|
|
273
|
+
def write(self, vals):
|
|
274
|
+
if "modification_ids" in vals:
|
|
275
|
+
res = super(
|
|
276
|
+
ContractContract, self.with_context(bypass_modification_send=True)
|
|
277
|
+
).write(vals)
|
|
278
|
+
self._modification_mail_send()
|
|
279
|
+
else:
|
|
280
|
+
res = super().write(vals)
|
|
281
|
+
return res
|
|
282
|
+
|
|
283
|
+
# === Actions ===
|
|
284
|
+
|
|
285
|
+
def action_preview(self):
|
|
286
|
+
"""Invoked when 'Preview' button in contract form view is clicked."""
|
|
287
|
+
self.ensure_one()
|
|
288
|
+
return {
|
|
289
|
+
"type": "ir.actions.act_url",
|
|
290
|
+
"target": "self",
|
|
291
|
+
"url": self.get_portal_url(),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
def action_show_invoices(self):
|
|
295
|
+
self.ensure_one()
|
|
296
|
+
tree_view = self.env.ref("account.view_invoice_tree", raise_if_not_found=False)
|
|
297
|
+
form_view = self.env.ref("account.view_move_form", raise_if_not_found=False)
|
|
298
|
+
ctx = dict(self.env.context)
|
|
299
|
+
if ctx.get("default_contract_type"):
|
|
300
|
+
ctx["default_move_type"] = (
|
|
301
|
+
"out_invoice"
|
|
302
|
+
if ctx.get("default_contract_type") == "sale"
|
|
303
|
+
else "in_invoice"
|
|
304
|
+
)
|
|
305
|
+
action = {
|
|
306
|
+
"type": "ir.actions.act_window",
|
|
307
|
+
"name": "Invoices",
|
|
308
|
+
"res_model": "account.move",
|
|
309
|
+
"view_mode": "list,kanban,form,calendar,pivot,graph,activity",
|
|
310
|
+
"domain": [("id", "in", self._get_related_invoices().ids)],
|
|
311
|
+
"context": ctx,
|
|
312
|
+
}
|
|
313
|
+
if tree_view and form_view:
|
|
314
|
+
action["views"] = [(tree_view.id, "list"), (form_view.id, "form")]
|
|
315
|
+
return action
|
|
316
|
+
|
|
317
|
+
def action_contract_send(self):
|
|
318
|
+
self.ensure_one()
|
|
319
|
+
template = self.env.ref("contract.email_contract_template", False)
|
|
320
|
+
compose_form = self.env.ref("mail.email_compose_message_wizard_form")
|
|
321
|
+
ctx = dict(
|
|
322
|
+
default_model="contract.contract",
|
|
323
|
+
default_res_ids=self.ids,
|
|
324
|
+
default_use_template=bool(template),
|
|
325
|
+
default_template_id=template and template.id or False,
|
|
326
|
+
default_composition_mode="comment",
|
|
327
|
+
)
|
|
328
|
+
return {
|
|
329
|
+
"name": self.env._("Compose Email"),
|
|
330
|
+
"type": "ir.actions.act_window",
|
|
331
|
+
"view_mode": "form",
|
|
332
|
+
"res_model": "mail.compose.message",
|
|
333
|
+
"views": [(compose_form.id, "form")],
|
|
334
|
+
"view_id": compose_form.id,
|
|
335
|
+
"target": "new",
|
|
336
|
+
"context": ctx,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
def recurring_create_invoice(self):
|
|
340
|
+
"""
|
|
341
|
+
This method triggers the creation of the next invoices of the contracts
|
|
342
|
+
even if their next invoicing date is in the future.
|
|
343
|
+
"""
|
|
344
|
+
invoices = self._recurring_create_invoice()
|
|
345
|
+
for invoice in invoices:
|
|
346
|
+
body = Markup(
|
|
347
|
+
self.env._("Contract manually invoiced: %(invoice_link)s")
|
|
348
|
+
) % {"invoice_link": invoice._get_html_link(title=invoice.name)}
|
|
349
|
+
self.message_post(body=body)
|
|
350
|
+
return invoices
|
|
351
|
+
|
|
352
|
+
# === Helpers and Utilities ===
|
|
353
|
+
|
|
354
|
+
def get_formview_id(self, access_uid=None):
|
|
355
|
+
if self.contract_type == "sale":
|
|
356
|
+
return self.env.ref("contract.contract_contract_customer_form_view").id
|
|
357
|
+
else:
|
|
358
|
+
return self.env.ref("contract.contract_contract_supplier_form_view").id
|
|
359
|
+
|
|
360
|
+
def _set_start_contract_modification(self):
|
|
361
|
+
subtype_id = self.env.ref("contract.mail_message_subtype_contract_modification")
|
|
362
|
+
for record in self:
|
|
363
|
+
if record.contract_line_ids:
|
|
364
|
+
date_start = min(record.contract_line_ids.mapped("date_start"))
|
|
365
|
+
else:
|
|
366
|
+
date_start = record.create_date
|
|
367
|
+
record.message_subscribe(
|
|
368
|
+
partner_ids=[record.partner_id.id], subtype_ids=[subtype_id.id]
|
|
369
|
+
)
|
|
370
|
+
record.with_context(skip_modification_mail=True).write(
|
|
371
|
+
{
|
|
372
|
+
"modification_ids": [
|
|
373
|
+
Command.create(
|
|
374
|
+
{
|
|
375
|
+
"date": date_start,
|
|
376
|
+
"description": self.env._("Contract start"),
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def _modification_mail_send(self):
|
|
384
|
+
for record in self:
|
|
385
|
+
modification_ids_not_sent = record.modification_ids.filtered(
|
|
386
|
+
lambda x: not x.sent
|
|
387
|
+
)
|
|
388
|
+
if modification_ids_not_sent:
|
|
389
|
+
if not self.env.context.get("skip_modification_mail"):
|
|
390
|
+
subtype_id = self.env["ir.model.data"]._xmlid_to_res_id(
|
|
391
|
+
"contract.mail_message_subtype_contract_modification"
|
|
392
|
+
)
|
|
393
|
+
template_id = self.env.ref(
|
|
394
|
+
"contract.mail_template_contract_modification"
|
|
395
|
+
)
|
|
396
|
+
record.message_post_with_source(
|
|
397
|
+
template_id,
|
|
398
|
+
subtype_id=subtype_id,
|
|
399
|
+
email_layout_xmlid="contract.template_contract_modification",
|
|
400
|
+
)
|
|
401
|
+
modification_ids_not_sent.write({"sent": True})
|
|
402
|
+
|
|
403
|
+
def _get_related_invoices(self):
|
|
404
|
+
self.ensure_one()
|
|
405
|
+
|
|
406
|
+
invoices = (
|
|
407
|
+
self.env["account.move.line"]
|
|
408
|
+
.search(
|
|
409
|
+
[
|
|
410
|
+
(
|
|
411
|
+
"contract_line_id",
|
|
412
|
+
"in",
|
|
413
|
+
self.contract_line_ids.ids,
|
|
414
|
+
)
|
|
415
|
+
]
|
|
416
|
+
)
|
|
417
|
+
.mapped("move_id")
|
|
418
|
+
)
|
|
419
|
+
# we are forced to always search for this for not losing possible <=v11
|
|
420
|
+
# generated invoices
|
|
421
|
+
invoices |= self.env["account.move"].search([("old_contract_id", "=", self.id)])
|
|
422
|
+
return invoices
|
|
423
|
+
|
|
424
|
+
def _get_computed_currency(self):
|
|
425
|
+
"""Helper method for returning the theoretical computed currency."""
|
|
426
|
+
self.ensure_one()
|
|
427
|
+
currency = self.env["res.currency"]
|
|
428
|
+
if any(self.contract_line_ids.mapped("automatic_price")):
|
|
429
|
+
# Use pricelist currency
|
|
430
|
+
currency = (
|
|
431
|
+
self.pricelist_id.currency_id
|
|
432
|
+
or self.partner_id.with_company(
|
|
433
|
+
self.company_id
|
|
434
|
+
).property_product_pricelist.currency_id
|
|
435
|
+
)
|
|
436
|
+
return currency or self.journal_id.currency_id or self.company_id.currency_id
|
|
437
|
+
|
|
395
438
|
def _convert_contract_lines(self, contract):
|
|
396
439
|
self.ensure_one()
|
|
397
440
|
new_lines = self.env["contract.line"]
|
|
@@ -403,7 +446,6 @@ class ContractContract(models.Model):
|
|
|
403
446
|
vals["date_start"] = fields.Date.context_today(contract_line)
|
|
404
447
|
vals["recurring_next_date"] = fields.Date.context_today(contract_line)
|
|
405
448
|
new_lines += contract_line_model.new(vals)
|
|
406
|
-
new_lines._onchange_is_auto_renew()
|
|
407
449
|
return new_lines
|
|
408
450
|
|
|
409
451
|
def _prepare_invoice(self, date_invoice, journal=None):
|
|
@@ -426,7 +468,7 @@ class ContractContract(models.Model):
|
|
|
426
468
|
)
|
|
427
469
|
if not journal:
|
|
428
470
|
raise ValidationError(
|
|
429
|
-
_(
|
|
471
|
+
self.env._(
|
|
430
472
|
"Please define a %(contract_type)s journal "
|
|
431
473
|
"for the company '%(company)s'."
|
|
432
474
|
)
|
|
@@ -469,28 +511,6 @@ class ContractContract(models.Model):
|
|
|
469
511
|
)
|
|
470
512
|
return vals
|
|
471
513
|
|
|
472
|
-
def action_contract_send(self):
|
|
473
|
-
self.ensure_one()
|
|
474
|
-
template = self.env.ref("contract.email_contract_template", False)
|
|
475
|
-
compose_form = self.env.ref("mail.email_compose_message_wizard_form")
|
|
476
|
-
ctx = dict(
|
|
477
|
-
default_model="contract.contract",
|
|
478
|
-
default_res_ids=self.ids,
|
|
479
|
-
default_use_template=bool(template),
|
|
480
|
-
default_template_id=template and template.id or False,
|
|
481
|
-
default_composition_mode="comment",
|
|
482
|
-
)
|
|
483
|
-
return {
|
|
484
|
-
"name": _("Compose Email"),
|
|
485
|
-
"type": "ir.actions.act_window",
|
|
486
|
-
"view_mode": "form",
|
|
487
|
-
"res_model": "mail.compose.message",
|
|
488
|
-
"views": [(compose_form.id, "form")],
|
|
489
|
-
"view_id": compose_form.id,
|
|
490
|
-
"target": "new",
|
|
491
|
-
"context": ctx,
|
|
492
|
-
}
|
|
493
|
-
|
|
494
514
|
@api.model
|
|
495
515
|
def _get_contracts_to_invoice_domain(self, date_ref=None):
|
|
496
516
|
"""
|
|
@@ -580,22 +600,9 @@ class ContractContract(models.Model):
|
|
|
580
600
|
)
|
|
581
601
|
invoices_values.append(invoice_vals)
|
|
582
602
|
# Force the recomputation of journal items
|
|
583
|
-
contract_lines.
|
|
603
|
+
contract_lines._update_last_date_invoiced()
|
|
584
604
|
return invoices_values
|
|
585
605
|
|
|
586
|
-
def recurring_create_invoice(self):
|
|
587
|
-
"""
|
|
588
|
-
This method triggers the creation of the next invoices of the contracts
|
|
589
|
-
even if their next invoicing date is in the future.
|
|
590
|
-
"""
|
|
591
|
-
invoices = self._recurring_create_invoice()
|
|
592
|
-
for invoice in invoices:
|
|
593
|
-
body = Markup(_("Contract manually invoiced: %(invoice_link)s")) % {
|
|
594
|
-
"invoice_link": invoice._get_html_link(title=invoice.name)
|
|
595
|
-
}
|
|
596
|
-
self.message_post(body=body)
|
|
597
|
-
return invoices
|
|
598
|
-
|
|
599
606
|
@api.model
|
|
600
607
|
def _invoice_followers(self, invoices):
|
|
601
608
|
invoice_create_subtype = self.env.ref(
|
|
@@ -614,7 +621,7 @@ class ContractContract(models.Model):
|
|
|
614
621
|
def _add_contract_origin(self, invoices):
|
|
615
622
|
for item in self:
|
|
616
623
|
for move in invoices & item._get_related_invoices():
|
|
617
|
-
translation = _("by contract")
|
|
624
|
+
translation = self.env._("by contract")
|
|
618
625
|
move.message_post(
|
|
619
626
|
body=Markup(
|
|
620
627
|
f"{move._creation_message()} {translation} "
|
|
@@ -674,52 +681,3 @@ class ContractContract(models.Model):
|
|
|
674
681
|
@api.model
|
|
675
682
|
def cron_recurring_create_invoice(self, date_ref=None):
|
|
676
683
|
return self._cron_recurring_create(date_ref, create_type="invoice")
|
|
677
|
-
|
|
678
|
-
def action_terminate_contract(self):
|
|
679
|
-
self.ensure_one()
|
|
680
|
-
context = {"default_contract_id": self.id}
|
|
681
|
-
return {
|
|
682
|
-
"type": "ir.actions.act_window",
|
|
683
|
-
"name": _("Terminate Contract"),
|
|
684
|
-
"res_model": "contract.contract.terminate",
|
|
685
|
-
"view_mode": "form",
|
|
686
|
-
"target": "new",
|
|
687
|
-
"context": context,
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
def _terminate_contract(
|
|
691
|
-
self,
|
|
692
|
-
terminate_reason_id,
|
|
693
|
-
terminate_comment,
|
|
694
|
-
terminate_date,
|
|
695
|
-
terminate_lines_with_last_date_invoiced=False,
|
|
696
|
-
):
|
|
697
|
-
self.ensure_one()
|
|
698
|
-
if not self.env.user.has_group("contract.can_terminate_contract"):
|
|
699
|
-
raise UserError(_("You are not allowed to terminate contracts."))
|
|
700
|
-
for line in self.contract_line_ids.filtered("is_stop_allowed"):
|
|
701
|
-
line.stop(
|
|
702
|
-
max(terminate_date, line.last_date_invoiced)
|
|
703
|
-
if terminate_lines_with_last_date_invoiced and line.last_date_invoiced
|
|
704
|
-
else terminate_date
|
|
705
|
-
)
|
|
706
|
-
self.write(
|
|
707
|
-
{
|
|
708
|
-
"is_terminated": True,
|
|
709
|
-
"terminate_reason_id": terminate_reason_id.id,
|
|
710
|
-
"terminate_comment": terminate_comment,
|
|
711
|
-
"terminate_date": terminate_date,
|
|
712
|
-
}
|
|
713
|
-
)
|
|
714
|
-
return True
|
|
715
|
-
|
|
716
|
-
def action_cancel_contract_termination(self):
|
|
717
|
-
self.ensure_one()
|
|
718
|
-
self.write(
|
|
719
|
-
{
|
|
720
|
-
"is_terminated": False,
|
|
721
|
-
"terminate_reason_id": False,
|
|
722
|
-
"terminate_comment": False,
|
|
723
|
-
"terminate_date": False,
|
|
724
|
-
}
|
|
725
|
-
)
|