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.
Files changed (130) hide show
  1. odoo/addons/contract/README.rst +14 -10
  2. odoo/addons/contract/__manifest__.py +3 -10
  3. odoo/addons/contract/controllers/main.py +1 -8
  4. odoo/addons/contract/data/contract_cron.xml +0 -2
  5. odoo/addons/contract/data/mail_template.xml +18 -17
  6. odoo/addons/contract/data/template_mail_notification.xml +1 -1
  7. odoo/addons/contract/i18n/am.po +146 -821
  8. odoo/addons/contract/i18n/ar.po +146 -821
  9. odoo/addons/contract/i18n/bg.po +146 -821
  10. odoo/addons/contract/i18n/bs.po +146 -821
  11. odoo/addons/contract/i18n/ca.po +835 -900
  12. odoo/addons/contract/i18n/ca_ES.po +146 -821
  13. odoo/addons/contract/i18n/contract.pot +145 -818
  14. odoo/addons/contract/i18n/cs.po +146 -821
  15. odoo/addons/contract/i18n/da.po +146 -821
  16. odoo/addons/contract/i18n/de.po +712 -953
  17. odoo/addons/contract/i18n/el_GR.po +146 -821
  18. odoo/addons/contract/i18n/en_GB.po +146 -821
  19. odoo/addons/contract/i18n/es.po +714 -947
  20. odoo/addons/contract/i18n/es_AR.po +550 -877
  21. odoo/addons/contract/i18n/es_CL.po +146 -821
  22. odoo/addons/contract/i18n/es_CO.po +146 -821
  23. odoo/addons/contract/i18n/es_CR.po +146 -821
  24. odoo/addons/contract/i18n/es_DO.po +146 -821
  25. odoo/addons/contract/i18n/es_EC.po +146 -821
  26. odoo/addons/contract/i18n/es_MX.po +146 -821
  27. odoo/addons/contract/i18n/es_PY.po +146 -821
  28. odoo/addons/contract/i18n/es_VE.po +146 -821
  29. odoo/addons/contract/i18n/et.po +146 -821
  30. odoo/addons/contract/i18n/eu.po +146 -821
  31. odoo/addons/contract/i18n/fa.po +146 -821
  32. odoo/addons/contract/i18n/fi.po +422 -848
  33. odoo/addons/contract/i18n/fr.po +713 -953
  34. odoo/addons/contract/i18n/fr_CA.po +146 -821
  35. odoo/addons/contract/i18n/fr_CH.po +146 -821
  36. odoo/addons/contract/i18n/fr_FR.po +454 -850
  37. odoo/addons/contract/i18n/gl.po +257 -846
  38. odoo/addons/contract/i18n/gl_ES.po +146 -821
  39. odoo/addons/contract/i18n/he.po +146 -821
  40. odoo/addons/contract/i18n/hi_IN.po +191 -831
  41. odoo/addons/contract/i18n/hr.po +211 -837
  42. odoo/addons/contract/i18n/hr_HR.po +223 -839
  43. odoo/addons/contract/i18n/hu.po +146 -821
  44. odoo/addons/contract/i18n/id.po +146 -821
  45. odoo/addons/contract/i18n/it.po +753 -902
  46. odoo/addons/contract/i18n/ja.po +146 -821
  47. odoo/addons/contract/i18n/ko.po +146 -821
  48. odoo/addons/contract/i18n/lt.po +146 -821
  49. odoo/addons/contract/i18n/lt_LT.po +146 -821
  50. odoo/addons/contract/i18n/lv.po +146 -821
  51. odoo/addons/contract/i18n/mk.po +146 -821
  52. odoo/addons/contract/i18n/mn.po +146 -821
  53. odoo/addons/contract/i18n/nb.po +146 -821
  54. odoo/addons/contract/i18n/nb_NO.po +146 -821
  55. odoo/addons/contract/i18n/nl.po +699 -953
  56. odoo/addons/contract/i18n/nl_BE.po +146 -821
  57. odoo/addons/contract/i18n/nl_NL.po +191 -831
  58. odoo/addons/contract/i18n/pl.po +146 -821
  59. odoo/addons/contract/i18n/pt.po +415 -839
  60. odoo/addons/contract/i18n/pt_BR.po +704 -947
  61. odoo/addons/contract/i18n/pt_PT.po +146 -821
  62. odoo/addons/contract/i18n/ro.po +146 -821
  63. odoo/addons/contract/i18n/ru.po +191 -831
  64. odoo/addons/contract/i18n/sk.po +146 -821
  65. odoo/addons/contract/i18n/sk_SK.po +146 -821
  66. odoo/addons/contract/i18n/sl.po +146 -821
  67. odoo/addons/contract/i18n/sr.po +146 -821
  68. odoo/addons/contract/i18n/sr@latin.po +146 -821
  69. odoo/addons/contract/i18n/sv.po +784 -933
  70. odoo/addons/contract/i18n/th.po +146 -821
  71. odoo/addons/contract/i18n/tr.po +611 -879
  72. odoo/addons/contract/i18n/tr_TR.po +221 -838
  73. odoo/addons/contract/i18n/uk.po +146 -821
  74. odoo/addons/contract/i18n/vi.po +146 -821
  75. odoo/addons/contract/i18n/vi_VN.po +146 -821
  76. odoo/addons/contract/i18n/zh_CN.po +407 -840
  77. odoo/addons/contract/i18n/zh_TW.po +150 -822
  78. odoo/addons/contract/migrations/18.0.2.0.0/end-migrate.py +27 -0
  79. odoo/addons/contract/migrations/18.0.2.0.0/pre-migrate.py +94 -0
  80. odoo/addons/contract/models/__init__.py +2 -6
  81. odoo/addons/contract/models/account_move.py +0 -8
  82. odoo/addons/contract/models/account_move_line.py +14 -0
  83. odoo/addons/contract/models/contract.py +272 -308
  84. odoo/addons/contract/models/contract_line.py +37 -859
  85. odoo/addons/contract/models/{contract_recurrency_mixin.py → contract_recurring_mixin.py} +101 -82
  86. odoo/addons/contract/models/contract_tag.py +1 -3
  87. odoo/addons/contract/models/contract_template.py +81 -2
  88. odoo/addons/contract/models/contract_template_line.py +250 -3
  89. odoo/addons/contract/report/contract_views.xml +0 -2
  90. odoo/addons/contract/report/report_contract.xml +13 -13
  91. odoo/addons/contract/security/contract_security.xml +6 -15
  92. odoo/addons/contract/security/contract_tag.xml +1 -3
  93. odoo/addons/contract/security/ir.model.access.csv +0 -2
  94. odoo/addons/contract/static/description/index.html +24 -18
  95. odoo/addons/contract/static/src/img/contract_icon.svg +4 -0
  96. odoo/addons/contract/static/src/js/contract_portal_tour.esm.js +6 -4
  97. odoo/addons/contract/tests/test_contract.py +82 -928
  98. odoo/addons/contract/tests/test_multicompany.py +5 -4
  99. odoo/addons/contract/tests/test_portal.py +6 -3
  100. odoo/addons/contract/views/contract.xml +92 -235
  101. odoo/addons/contract/views/contract_line.xml +48 -117
  102. odoo/addons/contract/views/contract_portal_templates.xml +187 -224
  103. odoo/addons/contract/views/contract_tag.xml +3 -3
  104. odoo/addons/contract/views/contract_template.xml +100 -72
  105. odoo/addons/contract/views/contract_template_line.xml +76 -5
  106. odoo/addons/contract/views/res_config_settings.xml +5 -6
  107. odoo/addons/contract/views/res_partner_view.xml +0 -5
  108. odoo/addons/contract/wizards/__init__.py +0 -2
  109. odoo/addons/contract/wizards/contract_manually_create_invoice.py +6 -6
  110. odoo/addons/contract/wizards/contract_manually_create_invoice.xml +2 -3
  111. {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/METADATA +18 -13
  112. odoo_addon_contract-18.0.2.0.8.dist-info/RECORD +132 -0
  113. {odoo_addon_contract-17.0.1.4.3.2.dist-info → odoo_addon_contract-18.0.2.0.8.dist-info}/WHEEL +1 -1
  114. odoo/addons/contract/data/contract_renew_cron.xml +0 -14
  115. odoo/addons/contract/models/abstract_contract.py +0 -82
  116. odoo/addons/contract/models/abstract_contract_line.py +0 -271
  117. odoo/addons/contract/models/contract_line_constraints.py +0 -429
  118. odoo/addons/contract/models/contract_terminate_reason.py +0 -14
  119. odoo/addons/contract/models/res_company.py +0 -15
  120. odoo/addons/contract/models/res_config_settings.py +0 -18
  121. odoo/addons/contract/security/contract_terminate_reason.xml +0 -23
  122. odoo/addons/contract/security/groups.xml +0 -9
  123. odoo/addons/contract/views/abstract_contract_line.xml +0 -117
  124. odoo/addons/contract/views/contract_terminate_reason.xml +0 -38
  125. odoo/addons/contract/wizards/contract_contract_terminate.py +0 -42
  126. odoo/addons/contract/wizards/contract_contract_terminate.xml +0 -33
  127. odoo/addons/contract/wizards/contract_line_wizard.py +0 -53
  128. odoo/addons/contract/wizards/contract_line_wizard.xml +0 -111
  129. odoo_addon_contract-17.0.1.4.3.2.dist-info/RECORD +0 -143
  130. {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 UserError, ValidationError
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
- active = fields.Boolean(
34
- default=True,
35
- )
36
- code = fields.Char(
37
- string="Reference",
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", comodel_name="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
- user_id = fields.Many2one(
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
- def get_formview_id(self, access_uid=None):
143
- if self.contract_type == "sale":
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
- @api.model
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
- super(ContractContract, contract)._compute_recurring_next_date()
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 self.contract_template_id[field_name]:
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._update_recurring_next_date()
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
- )