Files
contract/product_contract/models/sale_order_line.py
2023-05-04 16:41:52 +02:00

306 lines
12 KiB
Python

# Copyright 2017 LasLabs Inc.
# Copyright 2017 ACSONE SA/NV.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
is_contract = fields.Boolean(
string="Is a contract",
)
contract_id = fields.Many2one(
comodel_name="contract.contract", string="Contract", copy=False
)
contract_template_id = fields.Many2one(
comodel_name="contract.template",
string="Contract Template",
compute="_compute_contract_template_id",
)
recurring_rule_type = fields.Selection(
[
("daily", "Day(s)"),
("weekly", "Week(s)"),
("monthly", "Month(s)"),
("monthlylastday", "Month(s) last day"),
("quarterly", "Quarter(s)"),
("semesterly", "Semester(s)"),
("yearly", "Year(s)"),
],
default="monthly",
string="Invoice Every",
copy=False,
)
recurring_invoicing_type = fields.Selection(
[("pre-paid", "Pre-paid"), ("post-paid", "Post-paid")],
default="pre-paid",
string="Invoicing type",
help="Specify if process date is 'from' or 'to' invoicing date",
copy=False,
)
date_start = fields.Date(string="Date Start")
date_end = fields.Date(string="Date End")
contract_line_id = fields.Many2one(
comodel_name="contract.line",
string="Contract Line to replace",
required=False,
copy=False,
)
is_auto_renew = fields.Boolean(
string="Auto Renew",
compute="_compute_auto_renew",
default=False,
store=True,
readonly=False,
)
auto_renew_interval = fields.Integer(
default=1,
string="Renew Every",
compute="_compute_auto_renew",
store=True,
readonly=False,
help="Renew every (Days/Week/Month/Year)",
)
auto_renew_rule_type = fields.Selection(
[
("daily", "Day(s)"),
("weekly", "Week(s)"),
("monthly", "Month(s)"),
("yearly", "Year(s)"),
],
default="yearly",
compute="_compute_auto_renew",
store=True,
readonly=False,
string="Renewal type",
help="Specify Interval for automatic renewal.",
)
@api.constrains("contract_id")
def _check_contact_is_not_terminated(self):
for rec in self:
if (
rec.order_id.state not in ("sale", "done", "cancel")
and rec.contract_id.is_terminated
):
raise ValidationError(
_("You can't upsell or downsell a terminated contract")
)
@api.depends("product_id")
def _compute_contract_template_id(self):
for rec in self:
rec.contract_template_id = rec.product_id.with_company(
rec.order_id.company_id
).property_contract_template_id
def _get_auto_renew_rule_type(self):
"""monthly last day don't make sense for auto_renew_rule_type"""
self.ensure_one()
if self.recurring_rule_type == "monthlylastday":
return "monthly"
return self.recurring_rule_type
def _get_date_end(self):
self.ensure_one()
contract_line_model = self.env["contract.line"]
date_end = (
self.date_start
+ contract_line_model.get_relative_delta(
self._get_auto_renew_rule_type(),
int(self.product_uom_qty),
)
- relativedelta(days=1)
)
return date_end
@api.depends("product_id", "is_contract")
def _compute_auto_renew(self):
for rec in self:
if rec.is_contract:
rec.product_uom_qty = rec.product_id.default_qty
rec.recurring_rule_type = rec.product_id.recurring_rule_type
rec.recurring_invoicing_type = rec.product_id.recurring_invoicing_type
rec.date_start = rec.date_start or fields.Date.today()
rec.date_end = rec._get_date_end()
rec.is_auto_renew = rec.product_id.is_auto_renew
if rec.is_auto_renew:
rec.auto_renew_interval = rec.product_id.auto_renew_interval
rec.auto_renew_rule_type = rec.product_id.auto_renew_rule_type
@api.onchange("date_start", "product_uom_qty", "recurring_rule_type", "is_contract")
def onchange_date_start(self):
for rec in self.filtered("is_contract"):
rec.date_end = rec._get_date_end() if rec.date_start else False
@api.onchange("product_id")
def product_id_change(self):
super().product_id_change()
for rec in self:
if rec.product_id.is_contract:
rec.is_contract = True
else:
# Don't initialize wrong values
rec.is_contract = False
def _get_contract_line_qty(self):
"""Returns the quantity to be put on new contract lines."""
self.ensure_one()
# The quantity on the generated contract line is 1, as it
# correspond to the most common use cases:
# - quantity on the SO line = number of periods sold and unit
# price the price of one period, so the
# total amount of the SO corresponds to the planned value
# of the contract; in this case the quantity on the contract
# line must be 1
# - quantity on the SO line = number of hours sold,
# automatic invoicing of the actual hours through a variable
# quantity formula, in which case the quantity on the contract
# line is not used
# Other use cases are easy to implement by overriding this method.
return 1.0
def _prepare_contract_line_values(
self, contract, predecessor_contract_line_id=False
):
"""
:param contract: related contract
:param predecessor_contract_line_id: contract line to replace id
:return: new contract line dict
"""
self.ensure_one()
recurring_next_date = self.env[
"contract.line"
]._compute_first_recurring_next_date(
self.date_start or fields.Date.today(),
self.recurring_invoicing_type,
self.recurring_rule_type,
1,
)
termination_notice_interval = self.product_id.termination_notice_interval
termination_notice_rule_type = self.product_id.termination_notice_rule_type
return {
"sequence": self.sequence,
"product_id": self.product_id.id,
"name": self.name,
"quantity": self._get_contract_line_qty(),
"uom_id": self.product_uom.id,
"price_unit": self.price_unit,
"discount": self.discount,
"date_end": self.date_end,
"date_start": self.date_start or fields.Date.today(),
"recurring_next_date": recurring_next_date,
"recurring_interval": 1,
"recurring_invoicing_type": self.recurring_invoicing_type,
"recurring_rule_type": self.recurring_rule_type,
"is_auto_renew": self.is_auto_renew,
"auto_renew_interval": self.auto_renew_interval,
"auto_renew_rule_type": self.auto_renew_rule_type,
"termination_notice_interval": termination_notice_interval,
"termination_notice_rule_type": termination_notice_rule_type,
"contract_id": contract.id,
"sale_order_line_id": self.id,
"predecessor_contract_line_id": predecessor_contract_line_id,
"analytic_account_id": self.order_id.analytic_account_id.id,
}
def create_contract_line(self, contract):
contract_line_model = self.env["contract.line"]
contract_line = self.env["contract.line"]
predecessor_contract_line = False
for rec in self:
if rec.contract_line_id:
# If the upsell/downsell line start at the same date or before
# the contract line to replace supposed to start, we cancel
# the one to be replaced. Otherwise we stop it.
if rec.date_start <= rec.contract_line_id.date_start:
# The contract will handel the contract line integrity
# An exception will be raised if we try to cancel an
# invoiced contract line
rec.contract_line_id.cancel()
elif (
not rec.contract_line_id.date_end
or rec.date_start <= rec.contract_line_id.date_end
):
rec.contract_line_id.stop(rec.date_start - relativedelta(days=1))
predecessor_contract_line = rec.contract_line_id
if predecessor_contract_line:
new_contract_line = contract_line_model.create(
rec._prepare_contract_line_values(
contract, predecessor_contract_line.id
)
)
predecessor_contract_line.successor_contract_line_id = new_contract_line
else:
new_contract_line = contract_line_model.create(
rec._prepare_contract_line_values(contract)
)
contract_line |= new_contract_line
return contract_line
@api.constrains("contract_id")
def _check_contract_sale_partner(self):
for rec in self:
if rec.contract_id:
if rec.order_id.partner_id != rec.contract_id.partner_id:
raise ValidationError(
_(
"Sale Order and contract should be "
"linked to the same partner"
)
)
@api.constrains("product_id", "contract_id")
def _check_contract_sale_contract_template(self):
for rec in self:
if rec.contract_id:
if (
rec.contract_id.contract_template_id
and rec.contract_template_id != rec.contract_id.contract_template_id
):
raise ValidationError(
_("Contract product has different contract template")
)
@api.constrains("product_id", "contract_id")
def _check_contract_sale_line_is_contract(self):
for rec in self:
if rec.is_contract and not rec.product_id.is_contract:
raise ValidationError(
_(
'You can check the field "Is a contract" only for Contract product'
)
)
def _compute_invoice_status(self):
res = super()._compute_invoice_status()
self.filtered("contract_id").update({"invoice_status": "no"})
return res
def invoice_line_create(self, invoice_id, qty):
return super(
SaleOrderLine, self.filtered(lambda l: not l.contract_id)
).invoice_line_create(invoice_id, qty)
@api.depends(
"qty_invoiced",
"qty_delivered",
"product_uom_qty",
"order_id.state",
"is_contract",
)
def _get_to_invoice_qty(self):
"""
sale line linked to contracts must not be invoiced from sale order
"""
res = super()._get_to_invoice_qty()
self.filtered("is_contract").update({"qty_to_invoice": 0.0})
return res