odoo-addon-contract-line-successor 18.0.1.0.0.2__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 (33) hide show
  1. odoo/addons/contract_line_successor/README.rst +168 -0
  2. odoo/addons/contract_line_successor/__init__.py +2 -0
  3. odoo/addons/contract_line_successor/__manifest__.py +24 -0
  4. odoo/addons/contract_line_successor/data/contract_renew_cron.xml +12 -0
  5. odoo/addons/contract_line_successor/i18n/contract_line_successor.pot +532 -0
  6. odoo/addons/contract_line_successor/models/__init__.py +5 -0
  7. odoo/addons/contract_line_successor/models/contract_contract.py +25 -0
  8. odoo/addons/contract_line_successor/models/contract_line.py +835 -0
  9. odoo/addons/contract_line_successor/models/contract_line_constraints.py +429 -0
  10. odoo/addons/contract_line_successor/models/contract_template_line.py +35 -0
  11. odoo/addons/contract_line_successor/models/res_company.py +15 -0
  12. odoo/addons/contract_line_successor/models/res_config_settings.py +18 -0
  13. odoo/addons/contract_line_successor/readme/CONFIGURE.md +10 -0
  14. odoo/addons/contract_line_successor/readme/CONTRIBUTORS.md +1 -0
  15. odoo/addons/contract_line_successor/readme/DESCRIPTION.md +40 -0
  16. odoo/addons/contract_line_successor/readme/USAGE.md +6 -0
  17. odoo/addons/contract_line_successor/security/ir.model.access.csv +2 -0
  18. odoo/addons/contract_line_successor/static/description/icon.png +0 -0
  19. odoo/addons/contract_line_successor/static/description/index.html +523 -0
  20. odoo/addons/contract_line_successor/tests/__init__.py +1 -0
  21. odoo/addons/contract_line_successor/tests/test_contract.py +847 -0
  22. odoo/addons/contract_line_successor/views/contract_contract.xml +73 -0
  23. odoo/addons/contract_line_successor/views/contract_line.xml +134 -0
  24. odoo/addons/contract_line_successor/views/contract_template.xml +19 -0
  25. odoo/addons/contract_line_successor/views/contract_template_line.xml +50 -0
  26. odoo/addons/contract_line_successor/views/res_config_settings.xml +16 -0
  27. odoo/addons/contract_line_successor/wizards/__init__.py +1 -0
  28. odoo/addons/contract_line_successor/wizards/contract_line_wizard.py +53 -0
  29. odoo/addons/contract_line_successor/wizards/contract_line_wizard.xml +110 -0
  30. odoo_addon_contract_line_successor-18.0.1.0.0.2.dist-info/METADATA +185 -0
  31. odoo_addon_contract_line_successor-18.0.1.0.0.2.dist-info/RECORD +33 -0
  32. odoo_addon_contract_line_successor-18.0.1.0.0.2.dist-info/WHEEL +5 -0
  33. odoo_addon_contract_line_successor-18.0.1.0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,835 @@
1
+ # Copyright 2025 ACSONE SA/NV
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+
4
+ from datetime import timedelta
5
+
6
+ from dateutil.relativedelta import relativedelta
7
+ from markupsafe import Markup
8
+
9
+ from odoo import _, api, fields, models
10
+ from odoo.exceptions import ValidationError
11
+
12
+ from .contract_line_constraints import get_allowed
13
+
14
+
15
+ class ContractLine(models.Model):
16
+ _inherit = "contract.line"
17
+
18
+ termination_notice_date = fields.Date(
19
+ compute="_compute_termination_notice_date",
20
+ store=True,
21
+ copy=False,
22
+ )
23
+ successor_contract_line_id = fields.Many2one(
24
+ comodel_name="contract.line",
25
+ string="Successor Contract Line",
26
+ required=False,
27
+ readonly=True,
28
+ index=True,
29
+ copy=False,
30
+ help="In case of restart after suspension, this field contain the new "
31
+ "contract line created.",
32
+ )
33
+ predecessor_contract_line_id = fields.Many2one(
34
+ comodel_name="contract.line",
35
+ string="Predecessor Contract Line",
36
+ required=False,
37
+ readonly=True,
38
+ index=True,
39
+ copy=False,
40
+ help="Contract Line origin of this one.",
41
+ )
42
+ manual_renew_needed = fields.Boolean(
43
+ default=False,
44
+ help="This flag is used to make a difference between a definitive stop"
45
+ "and temporary one for which a user is not able to plan a"
46
+ "successor in advance",
47
+ )
48
+ is_plan_successor_allowed = fields.Boolean(
49
+ string="Plan successor allowed?", compute="_compute_allowed"
50
+ )
51
+ is_stop_plan_successor_allowed = fields.Boolean(
52
+ string="Stop/Plan successor allowed?", compute="_compute_allowed"
53
+ )
54
+ is_stop_allowed = fields.Boolean(string="Stop allowed?", compute="_compute_allowed")
55
+ is_cancel_allowed = fields.Boolean(
56
+ string="Cancel allowed?", compute="_compute_allowed"
57
+ )
58
+ is_un_cancel_allowed = fields.Boolean(
59
+ string="Un-Cancel allowed?", compute="_compute_allowed"
60
+ )
61
+ state = fields.Selection(
62
+ selection=[
63
+ ("upcoming", "Upcoming"),
64
+ ("in-progress", "In-progress"),
65
+ ("to-renew", "To renew"),
66
+ ("upcoming-close", "Upcoming Close"),
67
+ ("closed", "Closed"),
68
+ ("canceled", "Canceled"),
69
+ ],
70
+ compute="_compute_state",
71
+ search="_search_state",
72
+ )
73
+
74
+ @api.depends(
75
+ "date_end",
76
+ "termination_notice_rule_type",
77
+ "termination_notice_interval",
78
+ )
79
+ def _compute_termination_notice_date(self):
80
+ for rec in self:
81
+ if rec.date_end:
82
+ rec.termination_notice_date = rec.date_end - self.get_relative_delta(
83
+ rec.termination_notice_rule_type,
84
+ rec.termination_notice_interval,
85
+ )
86
+ else:
87
+ rec.termination_notice_date = False
88
+
89
+ @api.depends(
90
+ "date_start",
91
+ "date_end",
92
+ "last_date_invoiced",
93
+ "is_auto_renew",
94
+ "successor_contract_line_id",
95
+ "predecessor_contract_line_id",
96
+ "is_canceled",
97
+ )
98
+ def _compute_allowed(self):
99
+ for rec in self:
100
+ rec.update(
101
+ {
102
+ "is_plan_successor_allowed": False,
103
+ "is_stop_plan_successor_allowed": False,
104
+ "is_stop_allowed": False,
105
+ "is_cancel_allowed": False,
106
+ "is_un_cancel_allowed": False,
107
+ }
108
+ )
109
+ if rec.date_start:
110
+ allowed = get_allowed(
111
+ rec.date_start,
112
+ rec.date_end,
113
+ rec.last_date_invoiced,
114
+ rec.is_auto_renew,
115
+ rec.successor_contract_line_id,
116
+ rec.predecessor_contract_line_id,
117
+ rec.is_canceled,
118
+ )
119
+ if allowed:
120
+ rec.update(
121
+ {
122
+ "is_plan_successor_allowed": allowed.plan_successor,
123
+ "is_stop_plan_successor_allowed": (
124
+ allowed.stop_plan_successor
125
+ ),
126
+ "is_stop_allowed": allowed.stop,
127
+ "is_cancel_allowed": allowed.cancel,
128
+ "is_un_cancel_allowed": allowed.uncancel,
129
+ }
130
+ )
131
+
132
+ @api.constrains("is_auto_renew", "successor_contract_line_id", "date_end")
133
+ def _check_allowed(self):
134
+ """
135
+ logical impossible combination:
136
+ * a line with is_auto_renew True should have date_end and
137
+ couldn't have successor_contract_line_id
138
+ * a line without date_end can't have successor_contract_line_id
139
+
140
+ """
141
+ for rec in self:
142
+ if rec.is_auto_renew:
143
+ if rec.successor_contract_line_id:
144
+ raise ValidationError(
145
+ _(
146
+ "A contract line with a successor "
147
+ "can't be set to auto-renew"
148
+ )
149
+ )
150
+ if not rec.date_end:
151
+ raise ValidationError(_("An auto-renew line must have a end date"))
152
+ else:
153
+ if not rec.date_end and rec.successor_contract_line_id:
154
+ raise ValidationError(
155
+ _("A contract line with a successor " "must have a end date")
156
+ )
157
+
158
+ @api.constrains("successor_contract_line_id", "date_end")
159
+ def _check_overlap_successor(self):
160
+ for rec in self:
161
+ if rec.date_end and rec.successor_contract_line_id:
162
+ if rec.date_end >= rec.successor_contract_line_id.date_start:
163
+ raise ValidationError(
164
+ _("Contract line and its successor overlapped")
165
+ )
166
+
167
+ @api.constrains("predecessor_contract_line_id", "date_start")
168
+ def _check_overlap_predecessor(self):
169
+ for rec in self:
170
+ if (
171
+ rec.predecessor_contract_line_id
172
+ and rec.predecessor_contract_line_id.date_end
173
+ ):
174
+ if rec.date_start <= rec.predecessor_contract_line_id.date_end:
175
+ raise ValidationError(
176
+ _("Contract line and its predecessor overlapped")
177
+ )
178
+
179
+ @api.depends(
180
+ "is_canceled",
181
+ "date_start",
182
+ "date_end",
183
+ "is_auto_renew",
184
+ "manual_renew_needed",
185
+ "termination_notice_date",
186
+ "successor_contract_line_id",
187
+ )
188
+ def _compute_state(self):
189
+ today = fields.Date.context_today(self)
190
+ for rec in self:
191
+ rec.state = False
192
+ if rec.display_type:
193
+ continue
194
+ if rec.is_canceled:
195
+ rec.state = "canceled"
196
+ continue
197
+
198
+ if rec.date_start and rec.date_start > today:
199
+ # Before period
200
+ rec.state = "upcoming"
201
+ continue
202
+ if (
203
+ rec.date_start
204
+ and rec.date_start <= today
205
+ and (not rec.date_end or rec.date_end >= today)
206
+ ):
207
+ # In period
208
+ if (
209
+ rec.termination_notice_date
210
+ and rec.termination_notice_date < today
211
+ and not rec.is_auto_renew
212
+ and not rec.manual_renew_needed
213
+ ):
214
+ rec.state = "upcoming-close"
215
+ else:
216
+ rec.state = "in-progress"
217
+ continue
218
+ if rec.date_end and rec.date_end < today:
219
+ # After
220
+ if (
221
+ rec.manual_renew_needed
222
+ and not rec.successor_contract_line_id
223
+ or rec.is_auto_renew
224
+ ):
225
+ rec.state = "to-renew"
226
+ else:
227
+ rec.state = "closed"
228
+
229
+ @api.model
230
+ def _get_state_domain(self, state):
231
+ today = fields.Date.context_today(self)
232
+ if state == "upcoming":
233
+ return [
234
+ "&",
235
+ ("date_start", ">", today),
236
+ ("is_canceled", "=", False),
237
+ ]
238
+ if state == "in-progress":
239
+ return [
240
+ "&",
241
+ "&",
242
+ "&",
243
+ ("date_start", "<=", today),
244
+ ("is_canceled", "=", False),
245
+ "|",
246
+ ("date_end", ">=", today),
247
+ ("date_end", "=", False),
248
+ "|",
249
+ ("is_auto_renew", "=", True),
250
+ "&",
251
+ ("is_auto_renew", "=", False),
252
+ ("termination_notice_date", ">", today),
253
+ ]
254
+ if state == "to-renew":
255
+ return [
256
+ "&",
257
+ "&",
258
+ ("is_canceled", "=", False),
259
+ ("date_end", "<", today),
260
+ "|",
261
+ "&",
262
+ ("manual_renew_needed", "=", True),
263
+ ("successor_contract_line_id", "=", False),
264
+ ("is_auto_renew", "=", True),
265
+ ]
266
+ if state == "upcoming-close":
267
+ return [
268
+ "&",
269
+ "&",
270
+ "&",
271
+ "&",
272
+ "&",
273
+ ("date_start", "<=", today),
274
+ ("is_auto_renew", "=", False),
275
+ ("manual_renew_needed", "=", False),
276
+ ("is_canceled", "=", False),
277
+ ("termination_notice_date", "<", today),
278
+ ("date_end", ">=", today),
279
+ ]
280
+ if state == "closed":
281
+ return [
282
+ "&",
283
+ "&",
284
+ "&",
285
+ ("is_canceled", "=", False),
286
+ ("date_end", "<", today),
287
+ ("is_auto_renew", "=", False),
288
+ "|",
289
+ "&",
290
+ ("manual_renew_needed", "=", True),
291
+ ("successor_contract_line_id", "!=", False),
292
+ ("manual_renew_needed", "=", False),
293
+ ]
294
+ if state == "canceled":
295
+ return [("is_canceled", "=", True)]
296
+ if not state:
297
+ return [("display_type", "!=", False)]
298
+
299
+ @api.model
300
+ def _search_state(self, operator, value):
301
+ states = [
302
+ "upcoming",
303
+ "in-progress",
304
+ "to-renew",
305
+ "upcoming-close",
306
+ "closed",
307
+ "canceled",
308
+ False,
309
+ ]
310
+ if operator == "=":
311
+ return self._get_state_domain(value)
312
+ if operator == "!=":
313
+ domain = []
314
+ for state in states:
315
+ if state != value:
316
+ if domain:
317
+ domain.insert(0, "|")
318
+ domain.extend(self._get_state_domain(state))
319
+ return domain
320
+ if operator == "in":
321
+ domain = []
322
+ for state in value:
323
+ if domain:
324
+ domain.insert(0, "|")
325
+ domain.extend(self._get_state_domain(state))
326
+ return domain
327
+
328
+ if operator == "not in":
329
+ if set(value) == set(states):
330
+ return [("id", "=", False)]
331
+ return self._search_state(
332
+ "in", [state for state in states if state not in value]
333
+ )
334
+
335
+ @api.model
336
+ def _get_first_date_end(
337
+ self, date_start, auto_renew_rule_type, auto_renew_interval
338
+ ):
339
+ return (
340
+ date_start
341
+ + self.get_relative_delta(auto_renew_rule_type, auto_renew_interval)
342
+ - relativedelta(days=1)
343
+ )
344
+
345
+ @api.onchange(
346
+ "date_start",
347
+ "is_auto_renew",
348
+ "auto_renew_rule_type",
349
+ "auto_renew_interval",
350
+ )
351
+ def _onchange_is_auto_renew(self):
352
+ """Date end should be auto-computed if a contract line is set to
353
+ auto_renew"""
354
+ for rec in self.filtered("is_auto_renew"):
355
+ if rec.date_start:
356
+ rec.date_end = self._get_first_date_end(
357
+ rec.date_start,
358
+ rec.auto_renew_rule_type,
359
+ rec.auto_renew_interval,
360
+ )
361
+
362
+ @api.constrains("is_canceled", "is_auto_renew")
363
+ def _check_auto_renew_canceled_lines(self):
364
+ for rec in self:
365
+ if rec.is_canceled and rec.is_auto_renew:
366
+ raise ValidationError(
367
+ _("A canceled contract line can't be set to auto-renew")
368
+ )
369
+
370
+ def _delay(self, delay_delta):
371
+ """
372
+ Delay a contract line
373
+ :param delay_delta: delay relative delta
374
+ :return: delayed contract line
375
+ """
376
+ for rec in self:
377
+ if rec.last_date_invoiced:
378
+ raise ValidationError(
379
+ _("You can't delay a contract line " "invoiced at least one time.")
380
+ )
381
+ new_date_start = rec.date_start + delay_delta
382
+ if rec.date_end:
383
+ new_date_end = rec.date_end + delay_delta
384
+ else:
385
+ new_date_end = False
386
+ new_recurring_next_date = self.get_next_invoice_date(
387
+ new_date_start,
388
+ rec.recurring_invoicing_type,
389
+ rec.recurring_invoicing_offset,
390
+ rec.recurring_rule_type,
391
+ rec.recurring_interval,
392
+ max_date_end=new_date_end,
393
+ )
394
+ rec.write(
395
+ {
396
+ "date_start": new_date_start,
397
+ "date_end": new_date_end,
398
+ "recurring_next_date": new_recurring_next_date,
399
+ }
400
+ )
401
+
402
+ def _prepare_value_for_stop(self, date_end, manual_renew_needed):
403
+ self.ensure_one()
404
+ return {
405
+ "date_end": date_end,
406
+ "is_auto_renew": False,
407
+ "manual_renew_needed": manual_renew_needed,
408
+ "recurring_next_date": self.get_next_invoice_date(
409
+ self.next_period_date_start,
410
+ self.recurring_invoicing_type,
411
+ self.recurring_invoicing_offset,
412
+ self.recurring_rule_type,
413
+ self.recurring_interval,
414
+ max_date_end=date_end,
415
+ ),
416
+ }
417
+
418
+ def stop(self, date_end, manual_renew_needed=False, post_message=True):
419
+ """
420
+ Put date_end on contract line
421
+ We don't consider contract lines that end's before the new end date
422
+ :param date_end: new date end for contract line
423
+ :return: True
424
+ """
425
+ if not all(self.mapped("is_stop_allowed")):
426
+ raise ValidationError(_("Stop not allowed for this line"))
427
+ for rec in self:
428
+ if date_end < rec.date_start:
429
+ rec.cancel()
430
+ else:
431
+ if not rec.date_end or rec.date_end > date_end:
432
+ old_date_end = rec.date_end
433
+ rec.write(
434
+ rec._prepare_value_for_stop(date_end, manual_renew_needed)
435
+ )
436
+ if post_message:
437
+ msg = Markup(
438
+ _(
439
+ """Contract line for <strong>%(product)s</strong>
440
+ stopped: <br/>
441
+ - <strong>End</strong>: %(old_end)s -- %(new_end)s
442
+ """
443
+ )
444
+ ) % {
445
+ "product": rec.name,
446
+ "old_end": old_date_end,
447
+ "new_end": rec.date_end,
448
+ }
449
+ rec.contract_id.message_post(body=msg)
450
+ else:
451
+ rec.write(
452
+ {
453
+ "is_auto_renew": False,
454
+ "manual_renew_needed": manual_renew_needed,
455
+ }
456
+ )
457
+ return True
458
+
459
+ def _prepare_value_for_plan_successor(
460
+ self, date_start, date_end, is_auto_renew, recurring_next_date=False
461
+ ):
462
+ self.ensure_one()
463
+ if not recurring_next_date:
464
+ recurring_next_date = self.get_next_invoice_date(
465
+ date_start,
466
+ self.recurring_invoicing_type,
467
+ self.recurring_invoicing_offset,
468
+ self.recurring_rule_type,
469
+ self.recurring_interval,
470
+ max_date_end=date_end,
471
+ )
472
+ new_vals = self.read()[0]
473
+ new_vals.pop("id", None)
474
+ new_vals.pop("last_date_invoiced", None)
475
+ values = self._convert_to_write(new_vals)
476
+ values["date_start"] = date_start
477
+ values["date_end"] = date_end
478
+ values["recurring_next_date"] = recurring_next_date
479
+ values["is_auto_renew"] = is_auto_renew
480
+ values["predecessor_contract_line_id"] = self.id
481
+ return values
482
+
483
+ def plan_successor(
484
+ self,
485
+ date_start,
486
+ date_end,
487
+ is_auto_renew,
488
+ recurring_next_date=False,
489
+ post_message=True,
490
+ ):
491
+ """
492
+ Create a copy of a contract line in a new interval
493
+ :param date_start: date_start for the successor_contract_line
494
+ :param date_end: date_end for the successor_contract_line
495
+ :param is_auto_renew: is_auto_renew option for successor_contract_line
496
+ :param recurring_next_date: recurring_next_date for the
497
+ successor_contract_line
498
+ :return: successor_contract_line
499
+ """
500
+ contract_line = self.env["contract.line"]
501
+ for rec in self:
502
+ if not rec.is_plan_successor_allowed:
503
+ raise ValidationError(_("Plan successor not allowed for this line"))
504
+ rec.is_auto_renew = False
505
+ new_line = self.create(
506
+ rec._prepare_value_for_plan_successor(
507
+ date_start, date_end, is_auto_renew, recurring_next_date
508
+ )
509
+ )
510
+ rec.successor_contract_line_id = new_line
511
+ contract_line |= new_line
512
+ if post_message:
513
+ msg = Markup(
514
+ _(
515
+ """Contract line for <strong>%(product)s</strong>
516
+ planned a successor: <br/>
517
+ - <strong>Start</strong>: %(new_date_start)s
518
+ <br/>
519
+ - <strong>End</strong>: %(new_date_end)s
520
+ """
521
+ )
522
+ ) % {
523
+ "product": rec.name,
524
+ "new_date_start": new_line.date_start,
525
+ "new_date_end": new_line.date_end,
526
+ }
527
+ rec.contract_id.message_post(body=msg)
528
+ return contract_line
529
+
530
+ def stop_plan_successor(self, date_start, date_end, is_auto_renew):
531
+ """
532
+ Stop a contract line for a defined period and start it later
533
+ Cases to consider:
534
+ * contract line end's before the suspension period:
535
+ -> apply stop
536
+ * contract line start before the suspension period and end in it
537
+ -> apply stop at suspension start date
538
+ -> apply plan successor:
539
+ - date_start: suspension.date_end
540
+ - date_end: date_end + (contract_line.date_end
541
+ - suspension.date_start)
542
+ * contract line start before the suspension period and end after it
543
+ -> apply stop at suspension start date
544
+ -> apply plan successor:
545
+ - date_start: suspension.date_end
546
+ - date_end: date_end + (suspension.date_end
547
+ - suspension.date_start)
548
+ * contract line start and end's in the suspension period
549
+ -> apply delay
550
+ - delay: suspension.date_end - contract_line.date_start
551
+ * contract line start in the suspension period and end after it
552
+ -> apply delay
553
+ - delay: suspension.date_end - contract_line.date_start
554
+ * contract line start and end after the suspension period
555
+ -> apply delay
556
+ - delay: suspension.date_end - suspension.start_date
557
+ :param date_start: suspension start date
558
+ :param date_end: suspension end date
559
+ :param is_auto_renew: is the new line is set to auto_renew
560
+ :return: created contract line
561
+ """
562
+ if not all(self.mapped("is_stop_plan_successor_allowed")):
563
+ raise ValidationError(_("Stop/Plan successor not allowed for this line"))
564
+ contract_line = self.env["contract.line"]
565
+ for rec in self:
566
+ if rec.date_start >= date_start:
567
+ if rec.date_start < date_end:
568
+ delay = (date_end - rec.date_start) + timedelta(days=1)
569
+ else:
570
+ delay = (date_end - date_start) + timedelta(days=1)
571
+ rec._delay(delay)
572
+ contract_line |= rec
573
+ else:
574
+ if rec.date_end and rec.date_end < date_start:
575
+ rec.stop(date_start, post_message=False)
576
+ elif (
577
+ rec.date_end
578
+ and rec.date_end > date_start
579
+ and rec.date_end < date_end
580
+ ):
581
+ new_date_start = date_end + relativedelta(days=1)
582
+ new_date_end = (
583
+ date_end + (rec.date_end - date_start) + relativedelta(days=1)
584
+ )
585
+ rec.stop(
586
+ date_start - relativedelta(days=1),
587
+ manual_renew_needed=True,
588
+ post_message=False,
589
+ )
590
+ contract_line |= rec.plan_successor(
591
+ new_date_start,
592
+ new_date_end,
593
+ is_auto_renew,
594
+ post_message=False,
595
+ )
596
+ else:
597
+ new_date_start = date_end + relativedelta(days=1)
598
+ if rec.date_end:
599
+ new_date_end = (
600
+ rec.date_end
601
+ + (date_end - date_start)
602
+ + relativedelta(days=1)
603
+ )
604
+ else:
605
+ new_date_end = rec.date_end
606
+
607
+ rec.stop(
608
+ date_start - relativedelta(days=1),
609
+ manual_renew_needed=True,
610
+ post_message=False,
611
+ )
612
+ contract_line |= rec.plan_successor(
613
+ new_date_start,
614
+ new_date_end,
615
+ is_auto_renew,
616
+ post_message=False,
617
+ )
618
+ msg = Markup(
619
+ _(
620
+ """Contract line for <strong>%(product)s</strong>
621
+ suspended: <br/>
622
+ - <strong>Suspension Start</strong>: %(new_date_start)s
623
+ <br/>
624
+ - <strong>Suspension End</strong>: %(new_date_end)s
625
+ """
626
+ )
627
+ ) % {
628
+ "product": rec.name,
629
+ "new_date_start": date_start,
630
+ "new_date_end": date_end,
631
+ }
632
+ rec.contract_id.message_post(body=msg)
633
+ return contract_line
634
+
635
+ def cancel(self):
636
+ if not all(self.mapped("is_cancel_allowed")):
637
+ raise ValidationError(_("Cancel not allowed for this line"))
638
+ for contract in self.mapped("contract_id"):
639
+ lines = self.filtered(lambda line, c=contract: line.contract_id == c)
640
+ msg = Markup(
641
+ _(
642
+ "Contract line canceled: %s",
643
+ "<br/>- ".join(
644
+ [f"<strong>{name}</strong>" for name in lines.mapped("name")]
645
+ ),
646
+ )
647
+ )
648
+ contract.message_post(body=msg)
649
+ self.mapped("predecessor_contract_line_id").write(
650
+ {"successor_contract_line_id": False}
651
+ )
652
+ return self.write({"is_canceled": True, "is_auto_renew": False})
653
+
654
+ def uncancel(self, recurring_next_date):
655
+ if not all(self.mapped("is_un_cancel_allowed")):
656
+ raise ValidationError(_("Un-cancel not allowed for this line"))
657
+ for contract in self.mapped("contract_id"):
658
+ lines = self.filtered(lambda line, c=contract: line.contract_id == c)
659
+ msg = Markup(
660
+ _(
661
+ "Contract line Un-canceled: %s",
662
+ "<br/>- ".join(
663
+ [f"<strong>{name}</strong>" for name in lines.mapped("name")]
664
+ ),
665
+ )
666
+ )
667
+ contract.message_post(body=msg)
668
+ for rec in self:
669
+ if rec.predecessor_contract_line_id:
670
+ predecessor_contract_line = rec.predecessor_contract_line_id
671
+ assert not predecessor_contract_line.successor_contract_line_id
672
+ predecessor_contract_line.successor_contract_line_id = rec
673
+ rec.is_canceled = False
674
+ rec.recurring_next_date = recurring_next_date
675
+ return True
676
+
677
+ def action_uncancel(self):
678
+ self.ensure_one()
679
+ context = {
680
+ "default_contract_line_id": self.id,
681
+ "default_recurring_next_date": fields.Date.context_today(self),
682
+ }
683
+ context.update(self.env.context)
684
+ view_id = self.env.ref(
685
+ "contract_line_successor.contract_line_wizard_uncancel_form_view"
686
+ ).id
687
+ return {
688
+ "type": "ir.actions.act_window",
689
+ "name": "Un-Cancel Contract Line",
690
+ "res_model": "contract.line.wizard",
691
+ "view_mode": "form",
692
+ "views": [(view_id, "form")],
693
+ "target": "new",
694
+ "context": context,
695
+ }
696
+
697
+ def action_plan_successor(self):
698
+ self.ensure_one()
699
+ context = {
700
+ "default_contract_line_id": self.id,
701
+ "default_is_auto_renew": self.is_auto_renew,
702
+ }
703
+ context.update(self.env.context)
704
+ view_id = self.env.ref(
705
+ "contract_line_successor.contract_line_wizard_plan_successor_form_view"
706
+ ).id
707
+ return {
708
+ "type": "ir.actions.act_window",
709
+ "name": "Plan contract line successor",
710
+ "res_model": "contract.line.wizard",
711
+ "view_mode": "form",
712
+ "views": [(view_id, "form")],
713
+ "target": "new",
714
+ "context": context,
715
+ }
716
+
717
+ def action_stop(self):
718
+ self.ensure_one()
719
+ context = {
720
+ "default_contract_line_id": self.id,
721
+ "default_date_end": self.date_end,
722
+ }
723
+ context.update(self.env.context)
724
+ view_id = self.env.ref(
725
+ "contract_line_successor.contract_line_wizard_stop_form_view"
726
+ ).id
727
+ return {
728
+ "type": "ir.actions.act_window",
729
+ "name": "Terminate contract line",
730
+ "res_model": "contract.line.wizard",
731
+ "view_mode": "form",
732
+ "views": [(view_id, "form")],
733
+ "target": "new",
734
+ "context": context,
735
+ }
736
+
737
+ def action_stop_plan_successor(self):
738
+ self.ensure_one()
739
+ context = {
740
+ "default_contract_line_id": self.id,
741
+ "default_is_auto_renew": self.is_auto_renew,
742
+ }
743
+ context.update(self.env.context)
744
+ view_id = self.env.ref(
745
+ "contract_line_successor.contract_line_wizard_stop_plan_successor_form_view"
746
+ ).id
747
+ return {
748
+ "type": "ir.actions.act_window",
749
+ "name": "Suspend contract line",
750
+ "res_model": "contract.line.wizard",
751
+ "view_mode": "form",
752
+ "views": [(view_id, "form")],
753
+ "target": "new",
754
+ "context": context,
755
+ }
756
+
757
+ def _get_renewal_new_date_end(self):
758
+ self.ensure_one()
759
+ date_start = self.date_end + relativedelta(days=1)
760
+ date_end = self._get_first_date_end(
761
+ date_start, self.auto_renew_rule_type, self.auto_renew_interval
762
+ )
763
+ return date_end
764
+
765
+ def _renew_create_line(self, date_end):
766
+ self.ensure_one()
767
+ date_start = self.date_end + relativedelta(days=1)
768
+ is_auto_renew = self.is_auto_renew
769
+ self.stop(self.date_end, post_message=False)
770
+ new_line = self.plan_successor(
771
+ date_start, date_end, is_auto_renew, post_message=False
772
+ )
773
+ return new_line
774
+
775
+ def _renew_extend_line(self, date_end):
776
+ self.ensure_one()
777
+ self.date_end = date_end
778
+ return self
779
+
780
+ def renew(self):
781
+ res = self.env["contract.line"]
782
+ for rec in self:
783
+ company = rec.contract_id.company_id
784
+ date_end = rec._get_renewal_new_date_end()
785
+ date_start = rec.date_end + relativedelta(days=1)
786
+ if company.create_new_line_at_contract_line_renew:
787
+ new_line = rec._renew_create_line(date_end)
788
+ else:
789
+ new_line = rec._renew_extend_line(date_end)
790
+ res |= new_line
791
+ msg = Markup(
792
+ _(
793
+ """Contract line for <strong>%(product)s</strong>
794
+ renewed: <br/>
795
+ - <strong>Start</strong>: %(new_date_start)s
796
+ <br/>
797
+ - <strong>End</strong>: %(new_date_end)s
798
+ """
799
+ )
800
+ ) % {
801
+ "product": rec.name,
802
+ "new_date_start": date_start,
803
+ "new_date_end": date_end,
804
+ }
805
+ rec.contract_id.message_post(body=msg)
806
+ return res
807
+
808
+ @api.model
809
+ def _contract_line_to_renew_domain(self):
810
+ return [
811
+ ("is_auto_renew", "=", True),
812
+ ("is_canceled", "=", False),
813
+ ("termination_notice_date", "<=", fields.Date.context_today(self)),
814
+ ]
815
+
816
+ @api.model
817
+ def cron_renew_contract_line(self):
818
+ domain = self._contract_line_to_renew_domain()
819
+ to_renew = self.search(domain)
820
+ to_renew.renew()
821
+
822
+ def unlink(self):
823
+ """stop unlink uncnacled lines"""
824
+ for record in self:
825
+ if not (record.is_canceled or record.display_type):
826
+ raise ValidationError(_("Contract line must be canceled before delete"))
827
+ return super().unlink()
828
+
829
+ @api.constrains("is_auto_renew", "auto_renew_interval")
830
+ def _check_auto_renew_interval(self):
831
+ for rec in self:
832
+ if rec.is_auto_renew and not rec.auto_renew_interval:
833
+ raise ValidationError(
834
+ _("Auto renew interval should be different then 0")
835
+ )