flectra/addons/maintenance/models/maintenance.py

399 lines
21 KiB
Python

# -*- coding: utf-8 -*-
from datetime import date, datetime, timedelta
from flectra import api, fields, models, SUPERUSER_ID, _
from flectra.exceptions import UserError
from flectra.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
class MaintenanceStage(models.Model):
""" Model for case stages. This models the main stages of a Maintenance Request management flow. """
_name = 'maintenance.stage'
_description = 'Maintenance Stage'
_order = 'sequence, id'
name = fields.Char('Name', required=True, translate=True)
sequence = fields.Integer('Sequence', default=20)
fold = fields.Boolean('Folded in Maintenance Pipe')
done = fields.Boolean('Request Done')
class MaintenanceEquipmentCategory(models.Model):
_name = 'maintenance.equipment.category'
_inherit = ['mail.alias.mixin', 'mail.thread']
_description = 'Asset Category'
@api.one
@api.depends('equipment_ids')
def _compute_fold(self):
self.fold = False if self.equipment_count else True
name = fields.Char('Category Name', required=True, translate=True)
technician_user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid, oldname='user_id')
color = fields.Integer('Color Index')
note = fields.Text('Comments', translate=True)
equipment_ids = fields.One2many('maintenance.equipment', 'category_id', string='Equipments', copy=False)
equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count')
maintenance_ids = fields.One2many('maintenance.request', 'category_id', copy=False)
maintenance_count = fields.Integer(string="Maintenance", compute='_compute_maintenance_count')
alias_id = fields.Many2one(
'mail.alias', 'Alias', ondelete='restrict', required=True,
help="Email alias for this equipment category. New emails will automatically "
"create new maintenance request for this equipment category.")
fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True)
@api.multi
def _compute_equipment_count(self):
equipment_data = self.env['maintenance.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
for category in self:
category.equipment_count = mapped_data.get(category.id, 0)
@api.multi
def _compute_maintenance_count(self):
maintenance_data = self.env['maintenance.request'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data])
for category in self:
category.maintenance_count = mapped_data.get(category.id, 0)
@api.model
def create(self, vals):
self = self.with_context(alias_model_name='maintenance.request', alias_parent_model_name=self._name)
if not vals.get('alias_name'):
vals['alias_name'] = vals.get('name')
category_id = super(MaintenanceEquipmentCategory, self).create(vals)
category_id.alias_id.write({'alias_parent_thread_id': category_id.id, 'alias_defaults': {'category_id': category_id.id}})
return category_id
@api.multi
def unlink(self):
MailAlias = self.env['mail.alias']
for category in self:
if category.equipment_ids or category.maintenance_ids:
raise UserError(_("You cannot delete an equipment category containing equipments or maintenance requests."))
MailAlias += category.alias_id
res = super(MaintenanceEquipmentCategory, self).unlink()
MailAlias.unlink()
return res
def get_alias_model_name(self, vals):
return vals.get('alias_model', 'maintenance.equipment')
def get_alias_values(self):
values = super(MaintenanceEquipmentCategory, self).get_alias_values()
values['alias_defaults'] = {'category_id': self.id}
return values
class MaintenanceEquipment(models.Model):
_name = 'maintenance.equipment'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Equipment'
@api.multi
def _track_subtype(self, init_values):
self.ensure_one()
if 'owner_user_id' in init_values and self.owner_user_id:
return 'maintenance.mt_mat_assign'
return super(MaintenanceEquipment, self)._track_subtype(init_values)
@api.multi
def name_get(self):
result = []
for record in self:
if record.name and record.serial_no:
result.append((record.id, record.name + '/' + record.serial_no))
if record.name and not record.serial_no:
result.append((record.id, record.name))
return result
@api.model
def name_search(self, name, args=None, operator='ilike', limit=100):
args = args or []
recs = self.browse()
if name:
recs = self.search([('name', '=', name)] + args, limit=limit)
if not recs:
recs = self.search([('name', operator, name)] + args, limit=limit)
return recs.name_get()
name = fields.Char('Equipment Name', required=True, translate=True)
active = fields.Boolean(default=True)
technician_user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange', oldname='user_id')
owner_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange')
category_id = fields.Many2one('maintenance.equipment.category', string='Equipment Category',
track_visibility='onchange', group_expand='_read_group_category_ids')
partner_id = fields.Many2one('res.partner', string='Vendor', domain="[('supplier', '=', 1)]")
partner_ref = fields.Char('Vendor Reference')
location = fields.Char('Location')
model = fields.Char('Model')
serial_no = fields.Char('Serial Number', copy=False)
assign_date = fields.Date('Assigned Date', track_visibility='onchange')
cost = fields.Float('Cost')
note = fields.Text('Note')
warranty = fields.Date('Warranty')
color = fields.Integer('Color Index')
scrap_date = fields.Date('Scrap Date')
maintenance_ids = fields.One2many('maintenance.request', 'equipment_id')
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)
period = fields.Integer('Days between each preventive maintenance')
next_action_date = fields.Date(compute='_compute_next_maintenance', string='Date of the next preventive maintenance', store=True)
maintenance_team_id = fields.Many2one('maintenance.team', string='Maintenance Team')
maintenance_duration = fields.Float(help="Maintenance Duration in hours.")
@api.depends('period', 'maintenance_ids.request_date', 'maintenance_ids.close_date')
def _compute_next_maintenance(self):
date_now = fields.Date.context_today(self)
for equipment in self.filtered(lambda x: x.period > 0):
next_maintenance_todo = self.env['maintenance.request'].search([
('equipment_id', '=', equipment.id),
('maintenance_type', '=', 'preventive'),
('stage_id.done', '!=', True),
('close_date', '=', False)], order="request_date asc", limit=1)
last_maintenance_done = self.env['maintenance.request'].search([
('equipment_id', '=', equipment.id),
('maintenance_type', '=', 'preventive'),
('stage_id.done', '=', True),
('close_date', '!=', False)], order="close_date desc", limit=1)
if next_maintenance_todo and last_maintenance_done:
next_date = next_maintenance_todo.request_date
date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(last_maintenance_done.close_date)
# If the gap between the last_maintenance_done and the next_maintenance_todo one is bigger than 2 times the period and next request is in the future
# We use 2 times the period to avoid creation too closed request from a manually one created
if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2 and fields.Date.from_string(next_maintenance_todo.request_date) > fields.Date.from_string(date_now):
# If the new date still in the past, we set it for today
if fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period) < fields.Date.from_string(date_now):
next_date = date_now
else:
next_date = fields.Date.to_string(fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period))
elif next_maintenance_todo:
next_date = next_maintenance_todo.request_date
date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(date_now)
# If next maintenance to do is in the future, and in more than 2 times the period, we insert an new request
# We use 2 times the period to avoid creation too closed request from a manually one created
if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2:
next_date = fields.Date.to_string(fields.Date.from_string(date_now)+timedelta(days=equipment.period))
elif last_maintenance_done:
next_date = fields.Date.from_string(last_maintenance_done.close_date)+timedelta(days=equipment.period)
# If when we add the period to the last maintenance done and we still in past, we plan it for today
if next_date < fields.Date.from_string(date_now):
next_date = date_now
else:
next_date = fields.Date.to_string(fields.Date.from_string(date_now) + timedelta(days=equipment.period))
equipment.next_action_date = next_date
@api.one
@api.depends('maintenance_ids.stage_id.done')
def _compute_maintenance_count(self):
self.maintenance_count = len(self.maintenance_ids)
self.maintenance_open_count = len(self.maintenance_ids.filtered(lambda x: not x.stage_id.done))
@api.onchange('category_id')
def _onchange_category_id(self):
self.technician_user_id = self.category_id.technician_user_id
_sql_constraints = [
('serial_no', 'unique(serial_no)', "Another asset already exists with this serial number!"),
]
@api.model
def create(self, vals):
equipment = super(MaintenanceEquipment, self).create(vals)
if equipment.owner_user_id:
equipment.message_subscribe_users(user_ids=[equipment.owner_user_id.id])
return equipment
@api.multi
def write(self, vals):
if vals.get('owner_user_id'):
self.message_subscribe_users(user_ids=[vals['owner_user_id']])
return super(MaintenanceEquipment, self).write(vals)
@api.model
def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None):
""" mail.thread override so user_id which has no special access allowance is not
automatically subscribed.
"""
if auto_follow_fields is None:
auto_follow_fields = []
return super(MaintenanceEquipment, self)._message_get_auto_subscribe_fields(updated_fields, auto_follow_fields)
@api.model
def _read_group_category_ids(self, categories, domain, order):
""" Read group customization in order to display all the categories in
the kanban view, even if they are empty.
"""
category_ids = categories._search([], order=order, access_rights_uid=SUPERUSER_ID)
return categories.browse(category_ids)
def _create_new_request(self, date):
self.ensure_one()
self.env['maintenance.request'].create({
'name': _('Preventive Maintenance - %s') % self.name,
'request_date': date,
'schedule_date': date,
'category_id': self.category_id.id,
'equipment_id': self.id,
'maintenance_type': 'preventive',
'owner_user_id': self.owner_user_id.id,
'technician_user_id': self.technician_user_id.id,
'maintenance_team_id': self.maintenance_team_id.id,
'duration': self.maintenance_duration,
})
@api.model
def _cron_generate_requests(self):
"""
Generates maintenance request on the next_action_date or today if none exists
"""
for equipment in self.search([('period', '>', 0)]):
next_requests = self.env['maintenance.request'].search([('stage_id.done', '=', False),
('equipment_id', '=', equipment.id),
('maintenance_type', '=', 'preventive'),
('request_date', '=', equipment.next_action_date)])
if not next_requests:
equipment._create_new_request(equipment.next_action_date)
class MaintenanceRequest(models.Model):
_name = 'maintenance.request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Maintenance Requests'
_order = "id desc"
@api.returns('self')
def _default_stage(self):
return self.env['maintenance.stage'].search([], limit=1)
@api.multi
def _track_subtype(self, init_values):
self.ensure_one()
if 'stage_id' in init_values and self.stage_id.sequence <= 1:
return 'maintenance.mt_req_created'
elif 'stage_id' in init_values and self.stage_id.sequence > 1:
return 'maintenance.mt_req_status'
return super(MaintenanceRequest, self)._track_subtype(init_values)
def _get_default_team_id(self):
return self.env.ref('maintenance.equipment_team_maintenance', raise_if_not_found=False)
name = fields.Char('Subjects', required=True)
description = fields.Text('Description')
request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today,
help="Date requested for the maintenance to happen")
owner_user_id = fields.Many2one('res.users', string='Created by', default=lambda s: s.env.uid)
category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True)
equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True)
technician_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange', oldname='user_id')
stage_id = fields.Many2one('maintenance.stage', string='Stage', track_visibility='onchange',
group_expand='_read_group_stage_ids', default=_default_stage)
priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority')
color = fields.Integer('Color Index')
close_date = fields.Date('Close Date', help="Date the maintenance was finished. ")
kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')],
string='Kanban State', required=True, default='normal', track_visibility='onchange')
# active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.")
archive = fields.Boolean(default=False, help="Set archive to true to hide the maintenance request without deleting it.")
maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective")
schedule_date = fields.Datetime('Scheduled Date', help="Date the maintenance team plans the maintenance. It should not differ much from the Request Date. ")
maintenance_team_id = fields.Many2one('maintenance.team', string='Team', required=True, default=_get_default_team_id)
duration = fields.Float(help="Duration in minutes and seconds.")
@api.multi
def archive_equipment_request(self):
self.write({'archive': True})
@api.multi
def reset_equipment_request(self):
""" Reinsert the maintenance request into the maintenance pipe in the first stage"""
first_stage_obj = self.env['maintenance.stage'].search([], order="sequence asc", limit=1)
# self.write({'active': True, 'stage_id': first_stage_obj.id})
self.write({'archive': False, 'stage_id': first_stage_obj.id})
@api.onchange('equipment_id')
def onchange_equipment_id(self):
if self.equipment_id:
self.technician_user_id = self.equipment_id.technician_user_id if self.equipment_id.technician_user_id else self.equipment_id.category_id.technician_user_id
self.category_id = self.equipment_id.category_id
if self.equipment_id.maintenance_team_id:
self.maintenance_team_id = self.equipment_id.maintenance_team_id.id
@api.onchange('category_id')
def onchange_category_id(self):
if not self.technician_user_id or not self.equipment_id or (self.technician_user_id and not self.equipment_id.technician_user_id):
self.technician_user_id = self.category_id.technician_user_id
@api.model
def create(self, vals):
# context: no_log, because subtype already handle this
self = self.with_context(mail_create_nolog=True)
request = super(MaintenanceRequest, self).create(vals)
if request.owner_user_id or request.technician_user_id:
request._add_followers()
if request.equipment_id and not request.maintenance_team_id:
request.maintenance_team_id = request.equipment_id.maintenance_team_id
return request
@api.multi
def write(self, vals):
# Overridden to reset the kanban_state to normal whenever
# the stage (stage_id) of the Maintenance Request changes.
if vals and 'kanban_state' not in vals and 'stage_id' in vals:
vals['kanban_state'] = 'normal'
res = super(MaintenanceRequest, self).write(vals)
if vals.get('owner_user_id') or vals.get('technician_user_id'):
self._add_followers()
if self.stage_id.done and 'stage_id' in vals:
self.write({'close_date': fields.Date.today()})
return res
def _add_followers(self):
for request in self:
user_ids = (request.owner_user_id + request.technician_user_id).ids
request.message_subscribe_users(user_ids=user_ids)
@api.model
def _read_group_stage_ids(self, stages, domain, order):
""" Read group customization in order to display all the stages in the
kanban view, even if they are empty
"""
stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids)
class MaintenanceTeam(models.Model):
_name = 'maintenance.team'
_description = 'Maintenance Teams'
name = fields.Char(required=True, translate=True)
member_ids = fields.Many2many('res.users', 'maintenance_team_users_rel', string="Team Members")
color = fields.Integer("Color Index", default=0)
request_ids = fields.One2many('maintenance.request', 'maintenance_team_id', copy=False)
equipment_ids = fields.One2many('maintenance.equipment', 'maintenance_team_id', copy=False)
# For the dashboard only
todo_request_ids = fields.One2many('maintenance.request', string="Requests", copy=False, compute='_compute_todo_requests')
todo_request_count = fields.Integer(string="Number of Requests", compute='_compute_todo_requests')
todo_request_count_date = fields.Integer(string="Number of Requests Scheduled", compute='_compute_todo_requests')
todo_request_count_high_priority = fields.Integer(string="Number of Requests in High Priority", compute='_compute_todo_requests')
todo_request_count_block = fields.Integer(string="Number of Requests Blocked", compute='_compute_todo_requests')
todo_request_count_unscheduled = fields.Integer(string="Number of Requests Unscheduled", compute='_compute_todo_requests')
@api.one
@api.depends('request_ids.stage_id.done')
def _compute_todo_requests(self):
self.todo_request_ids = self.request_ids.filtered(lambda e: e.stage_id.done==False)
self.todo_request_count = len(self.todo_request_ids)
self.todo_request_count_date = len(self.todo_request_ids.filtered(lambda e: e.schedule_date != False))
self.todo_request_count_high_priority = len(self.todo_request_ids.filtered(lambda e: e.priority == '3'))
self.todo_request_count_block = len(self.todo_request_ids.filtered(lambda e: e.kanban_state == 'blocked'))
self.todo_request_count_unscheduled = len(self.todo_request_ids.filtered(lambda e: not e.schedule_date))
@api.one
@api.depends('equipment_ids')
def _compute_equipment(self):
self.equipment_count = len(self.equipment_ids)