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.

248 lines
9.5 KiB

  1. # Copyright 2017 LasLabs Inc.
  2. # Copyright 2017 ACSONE SA/NV.
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. from dateutil.relativedelta import relativedelta
  5. from odoo import api, fields, models, _
  6. from odoo.exceptions import ValidationError
  7. class SaleOrderLine(models.Model):
  8. _inherit = 'sale.order.line'
  9. is_contract = fields.Boolean(
  10. string='Is a contract', related="product_id.is_contract"
  11. )
  12. contract_id = fields.Many2one(
  13. comodel_name='contract.contract', string='Contract', copy=False
  14. )
  15. contract_template_id = fields.Many2one(
  16. comodel_name='contract.template',
  17. string='Contract Template',
  18. related='product_id.product_tmpl_id.contract_template_id',
  19. readonly=True,
  20. )
  21. recurring_rule_type = fields.Selection(
  22. [
  23. ('daily', 'Day(s)'),
  24. ('weekly', 'Week(s)'),
  25. ('monthly', 'Month(s)'),
  26. ('monthlylastday', 'Month(s) last day'),
  27. ('yearly', 'Year(s)'),
  28. ],
  29. default='monthly',
  30. string='Invoice Every',
  31. copy=False,
  32. )
  33. recurring_invoicing_type = fields.Selection(
  34. [('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')],
  35. default='pre-paid',
  36. string='Invoicing type',
  37. help="Specify if process date is 'from' or 'to' invoicing date",
  38. copy=False,
  39. )
  40. date_start = fields.Date(string='Date Start')
  41. date_end = fields.Date(string='Date End')
  42. contract_line_id = fields.Many2one(
  43. comodel_name="contract.line",
  44. string="Contract Line to replace",
  45. required=False,
  46. copy=False,
  47. )
  48. @api.multi
  49. def _get_auto_renew_rule_type(self):
  50. """monthly last day don't make sense for auto_renew_rule_type"""
  51. self.ensure_one()
  52. if self.recurring_rule_type == "monthlylastday":
  53. return "monthly"
  54. return self.recurring_rule_type
  55. @api.onchange('product_id')
  56. def onchange_product(self):
  57. contract_line_model = self.env['contract.line']
  58. for rec in self:
  59. if rec.product_id.is_contract:
  60. rec.product_uom_qty = rec.product_id.default_qty
  61. rec.recurring_rule_type = rec.product_id.recurring_rule_type
  62. rec.recurring_invoicing_type = (
  63. rec.product_id.recurring_invoicing_type
  64. )
  65. rec.date_start = rec.date_start or fields.Date.today()
  66. rec.date_end = (
  67. rec.date_start
  68. + contract_line_model.get_relative_delta(
  69. rec._get_auto_renew_rule_type(),
  70. int(rec.product_uom_qty),
  71. )
  72. - relativedelta(days=1)
  73. )
  74. @api.onchange('date_start', 'product_uom_qty', 'recurring_rule_type')
  75. def onchange_date_start(self):
  76. contract_line_model = self.env['contract.line']
  77. for rec in self.filtered('product_id.is_contract'):
  78. if not rec.date_start:
  79. rec.date_end = False
  80. else:
  81. rec.date_end = (
  82. rec.date_start
  83. + contract_line_model.get_relative_delta(
  84. rec._get_auto_renew_rule_type(),
  85. int(rec.product_uom_qty),
  86. )
  87. - relativedelta(days=1)
  88. )
  89. @api.multi
  90. def _prepare_contract_line_values(
  91. self, contract, predecessor_contract_line_id=False
  92. ):
  93. """
  94. :param contract: related contract
  95. :param predecessor_contract_line_id: contract line to replace id
  96. :return: new contract line dict
  97. """
  98. self.ensure_one()
  99. recurring_next_date = self.env[
  100. 'contract.line'
  101. ]._compute_first_recurring_next_date(
  102. self.date_start or fields.Date.today(),
  103. self.recurring_invoicing_type,
  104. self.recurring_rule_type,
  105. 1,
  106. )
  107. termination_notice_interval = (
  108. self.product_id.termination_notice_interval
  109. )
  110. termination_notice_rule_type = (
  111. self.product_id.termination_notice_rule_type
  112. )
  113. return {
  114. 'sequence': self.sequence,
  115. 'product_id': self.product_id.id,
  116. 'name': self.name,
  117. # The quantity on the generated contract line is 1, as it
  118. # correspond to the most common use cases:
  119. # - quantity on the SO line = number of periods sold and unit
  120. # price the price of one period, so the
  121. # total amount of the SO corresponds to the planned value
  122. # of the contract; in this case the quantity on the contract
  123. # line must be 1
  124. # - quantity on the SO line = number of hours sold,
  125. # automatic invoicing of the actual hours through a variable
  126. # quantity formula, in which case the quantity on the contract
  127. # line is not used
  128. # Other use cases are easy to implement by overriding this method.
  129. 'quantity': 1.0,
  130. 'uom_id': self.product_uom.id,
  131. 'price_unit': self.price_unit,
  132. 'discount': self.discount,
  133. 'date_end': self.date_end,
  134. 'date_start': self.date_start or fields.Date.today(),
  135. 'recurring_next_date': recurring_next_date,
  136. 'recurring_interval': 1,
  137. 'recurring_invoicing_type': self.recurring_invoicing_type,
  138. 'recurring_rule_type': self.recurring_rule_type,
  139. 'is_auto_renew': self.product_id.is_auto_renew,
  140. 'auto_renew_interval': self.product_uom_qty,
  141. 'auto_renew_rule_type': self._get_auto_renew_rule_type(),
  142. 'termination_notice_interval': termination_notice_interval,
  143. 'termination_notice_rule_type': termination_notice_rule_type,
  144. 'contract_id': contract.id,
  145. 'sale_order_line_id': self.id,
  146. 'predecessor_contract_line_id': predecessor_contract_line_id,
  147. }
  148. @api.multi
  149. def create_contract_line(self, contract):
  150. contract_line_model = self.env['contract.line']
  151. contract_line = self.env['contract.line']
  152. predecessor_contract_line = False
  153. for rec in self:
  154. if rec.contract_line_id:
  155. # If the upsell/downsell line start at the same date or before
  156. # the contract line to replace supposed to start, we cancel
  157. # the one to be replaced. Otherwise we stop it.
  158. if rec.date_start <= rec.contract_line_id.date_start:
  159. # The contract will handel the contract line integrity
  160. # An exception will be raised if we try to cancel an
  161. # invoiced contract line
  162. rec.contract_line_id.cancel()
  163. elif (
  164. not rec.contract_line_id.date_end
  165. or rec.date_start <= rec.contract_line_id.date_end
  166. ):
  167. rec.contract_line_id.stop(
  168. rec.date_start - relativedelta(days=1)
  169. )
  170. predecessor_contract_line = rec.contract_line_id
  171. if predecessor_contract_line:
  172. new_contract_line = contract_line_model.create(
  173. rec._prepare_contract_line_values(
  174. contract, predecessor_contract_line.id
  175. )
  176. )
  177. predecessor_contract_line.successor_contract_line_id = (
  178. new_contract_line
  179. )
  180. else:
  181. new_contract_line = contract_line_model.create(
  182. rec._prepare_contract_line_values(contract)
  183. )
  184. contract_line |= new_contract_line
  185. return contract_line
  186. @api.constrains('contract_id')
  187. def _check_contract_sale_partner(self):
  188. for rec in self:
  189. if rec.contract_id:
  190. if rec.order_id.partner_id != rec.contract_id.partner_id:
  191. raise ValidationError(
  192. _(
  193. "Sale Order and contract should be "
  194. "linked to the same partner"
  195. )
  196. )
  197. @api.constrains('product_id', 'contract_id')
  198. def _check_contract_sale_contract_template(self):
  199. for rec in self:
  200. if rec.contract_id:
  201. if (
  202. rec.contract_id.contract_template_id
  203. and rec.contract_template_id
  204. != rec.contract_id.contract_template_id
  205. ):
  206. raise ValidationError(
  207. _("Contract product has different contract template")
  208. )
  209. def _compute_invoice_status(self):
  210. res = super(SaleOrderLine, self)._compute_invoice_status()
  211. for line in self.filtered('contract_id'):
  212. line.invoice_status = 'no'
  213. return res
  214. @api.multi
  215. def invoice_line_create(self, invoice_id, qty):
  216. return super(
  217. SaleOrderLine, self.filtered(lambda l: not l.contract_id)
  218. ).invoice_line_create(invoice_id, qty)
  219. @api.depends(
  220. 'qty_invoiced',
  221. 'qty_delivered',
  222. 'product_uom_qty',
  223. 'order_id.state',
  224. 'product_id.is_contract',
  225. )
  226. def _get_to_invoice_qty(self):
  227. """
  228. sale line linked to contracts must not be invoiced from sale order
  229. """
  230. res = super()._get_to_invoice_qty()
  231. self.filtered('product_id.is_contract').update({'qty_to_invoice': 0.0})
  232. return res