You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

909 lines
34 KiB

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