oca-technical/odoo-bringout-oca-maintenance-maintenance_plan/maintenance_plan/models/maintenance_plan.py
2025-08-29 15:43:03 +02:00

261 lines
9.5 KiB
Python

# Copyright 2017 Camptocamp SA
# Copyright 2019-20 ForgeFlow S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import safe_eval
class MaintenancePlan(models.Model):
_name = "maintenance.plan"
_description = "Maintenance Plan"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char("Description")
active = fields.Boolean(default=True)
equipment_id = fields.Many2one(
string="Equipment", comodel_name="maintenance.equipment", ondelete="cascade"
)
company_id = fields.Many2one(
comodel_name="res.company",
default=lambda self: self.env.company,
)
maintenance_kind_id = fields.Many2one(
string="Maintenance Kind", comodel_name="maintenance.kind", ondelete="restrict"
)
interval = fields.Integer(
string="Frequency", default=1, help="Interval between each maintenance"
)
interval_step = fields.Selection(
[
("day", "Day(s)"),
("week", "Week(s)"),
("month", "Month(s)"),
("year", "Year(s)"),
],
string="Recurrence",
default="year",
help="Let the event automatically repeat at that interval step",
)
duration = fields.Float(
string="Duration (hours)", help="Maintenance duration in hours"
)
start_maintenance_date = fields.Date(
default=fields.Date.context_today,
help="Date from which the maintenance will we active",
)
next_maintenance_date = fields.Date(compute="_compute_next_maintenance", store=True)
maintenance_plan_horizon = fields.Integer(
string="Planning Horizon period",
default=1,
help="Maintenance planning horizon. Only the maintenance requests "
"inside the horizon will be created.",
)
planning_step = fields.Selection(
[
("day", "Day(s)"),
("week", "Week(s)"),
("month", "Month(s)"),
("year", "Year(s)"),
],
string="Planning Horizon step",
default="year",
help="Let the event automatically repeat at that interval",
)
note = fields.Html()
maintenance_ids = fields.One2many(
"maintenance.request", "maintenance_plan_id", string="Maintenance requests"
)
maintenance_count = fields.Integer(
compute="_compute_maintenance_count", string="Maintenance", store=True
)
maintenance_open_count = fields.Integer(
compute="_compute_maintenance_count", string="Current Maintenance", store=True
)
maintenance_team_id = fields.Many2one("maintenance.team")
skip_notify_follower_on_requests = fields.Boolean(
string="Do not notify to follower when creating requests?", default=True
)
generate_with_domain = fields.Boolean()
generate_domain = fields.Char(string="Apply on")
search_equipment_id = fields.Many2one(
comodel_name="maintenance.equipment",
compute="_compute_search_equipment",
search="_search_search_equipment",
)
@api.model
def _search_search_equipment(self, operator, value):
if operator != "=" or (not value and not isinstance(value, models.NewId)):
raise ValueError(_("Unsupported search operator"))
plans = self.search([("generate_with_domain", "=", True)])
plan_ids = []
equipment = self.env["maintenance.equipment"].browse(value)
for plan in plans:
if equipment.filtered_domain(
safe_eval.safe_eval(
plan.generate_domain or "[]", plan._get_eval_context()
)
):
plan_ids.append(plan.id)
return ["|", ("equipment_id", "=", value), ("id", "in", plan_ids)]
@api.depends("equipment_id")
def _compute_search_equipment(self):
for record in self:
record.search_equipment_id = record.equipment_id
def _get_eval_context(self):
"""Prepare the context used when evaluating python code
:returns: dict -- evaluation context given to safe_eval
"""
return {
"datetime": safe_eval.datetime,
"dateutil": safe_eval.dateutil,
"time": safe_eval.time,
}
def name_get(self):
result = []
for plan in self:
result.append(
(
plan.id,
plan.name
or _(
"Unnamed %(kind)s plan (%(eqpmt)s)",
kind=plan.maintenance_kind_id.name or "",
eqpmt=plan.equipment_id.name,
),
)
)
return result
@api.depends("maintenance_ids.stage_id.done")
def _compute_maintenance_count(self):
for equipment in self:
equipment.maintenance_count = len(equipment.maintenance_ids)
equipment.maintenance_open_count = len(
equipment.maintenance_ids.filtered(lambda x: not x.stage_id.done)
)
def get_relativedelta(self, interval, step):
if step == "day":
return relativedelta(days=interval)
elif step == "week":
return relativedelta(weeks=interval)
elif step == "month":
return relativedelta(months=interval)
elif step == "year":
return relativedelta(years=interval)
@api.depends(
"interval",
"interval_step",
"start_maintenance_date",
"maintenance_ids.request_date",
"maintenance_ids.close_date",
)
def _compute_next_maintenance(self):
for plan in self.filtered(lambda x: x.interval > 0):
interval_timedelta = plan.get_relativedelta(
plan.interval, plan.interval_step
)
next_maintenance_todo = self.env["maintenance.request"].search(
[
("maintenance_plan_id", "=", plan.id),
("stage_id.done", "!=", True),
("close_date", "=", False),
("request_date", ">=", plan.start_maintenance_date),
],
order="request_date asc",
limit=1,
)
if next_maintenance_todo:
plan.next_maintenance_date = next_maintenance_todo.request_date
else:
last_maintenance_done = self.env["maintenance.request"].search(
[
("maintenance_plan_id", "=", plan.id),
("request_date", ">=", plan.start_maintenance_date),
],
order="request_date desc",
limit=1,
)
if last_maintenance_done:
plan.next_maintenance_date = (
last_maintenance_done.request_date + interval_timedelta
)
else:
next_date = plan.start_maintenance_date
while next_date < fields.Date.today():
next_date = next_date + interval_timedelta
plan.next_maintenance_date = next_date
@api.constrains("company_id", "equipment_id")
def _check_company_id(self):
for rec in self:
if (
rec.equipment_id.company_id
and rec.company_id != rec.equipment_id.company_id
):
raise ValidationError(
_("Maintenace Equipment must belong to the equipment's company")
)
def unlink(self):
"""Restrict deletion of maintenance plan should there be maintenance
requests of this kind which are not done for its equipment"""
for plan in self:
request = plan.equipment_id.mapped("maintenance_ids").filtered(
lambda r: (
r.maintenance_kind_id == plan.maintenance_kind_id
and not r.stage_id.done
and r.maintenance_type == "preventive"
)
)
if request:
raise UserError(
_(
"The maintenance plan %(kind)s of equipment %(eqpmnt)s "
"has generated a request which is not done "
"yet. You should either set the request as "
"done, remove its maintenance kind or "
"delete it first.",
kind=plan.maintenance_kind_id.name,
eqpmnt=plan.equipment_id.name,
)
)
return super().unlink()
_sql_constraints = [
(
"equipment_kind_uniq",
"unique (equipment_id, maintenance_kind_id)",
"You cannot define multiple times the same maintenance kind on an "
"equipment maintenance plan.",
)
]
def button_manual_request_generation(self):
"""Call the same method that the cron for generating manually the maintenance
requests."""
for plan in self:
equipment = plan.equipment_id
equipment._create_new_request(plan)
def _get_maintenance_equipments(self):
self.ensure_one()
if self.generate_with_domain and not self.equipment_id:
return self.env["maintenance.equipment"].search(
safe_eval.safe_eval(
self.generate_domain or "[]", self._get_eval_context()
)
)
return [self.equipment_id]