The issue is that if you block a workorder when you exceeded the expected time, the workcenter is not blocked. The workcenter state is computed according to its productivity lines. The productivy lines are marked with a loss type type that define the reason of the time loss (availability problem like missing equipment or quality problem like broken equipment). There are also two magic loss type that define if the work on the workcenter is done in the expected time (fully productive time) or is done exceeding the expected time (reduced speed). The unblock button appears if the workcenter is blocked. To compute the workcenter state, we have to look through the last productivity line of the workcenter having no end_date. Before the patch, we look for the last productivity line and it could return one with an end_date while there was a productivity line without one and it was this one that would define the workcenter state. This is solved by immediately searching for a record without end date. We also add some comment to the code to understand a little more what's going on.
215 lines
11 KiB
Python
215 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from dateutil import relativedelta
|
|
import datetime
|
|
|
|
from odoo import api, exceptions, fields, models, _
|
|
|
|
|
|
class MrpWorkcenter(models.Model):
|
|
_name = 'mrp.workcenter'
|
|
_description = 'Work Center'
|
|
_inherits = {'resource.resource': 'resource_id'}
|
|
_order = "sequence, id"
|
|
|
|
note = fields.Text(
|
|
'Description',
|
|
help="Description of the Work Center.")
|
|
capacity = fields.Float(
|
|
'Capacity', default=1.0, oldname='capacity_per_cycle',
|
|
help="Number of pieces that can be produced in parallel.")
|
|
sequence = fields.Integer(
|
|
'Sequence', default=1, required=True,
|
|
help="Gives the sequence order when displaying a list of work centers.")
|
|
color = fields.Integer('Color')
|
|
time_start = fields.Float('Time before prod.', help="Time in minutes for the setup.")
|
|
time_stop = fields.Float('Time after prod.', help="Time in minutes for the cleaning.")
|
|
resource_id = fields.Many2one('resource.resource', 'Resource', ondelete='cascade', required=True)
|
|
routing_line_ids = fields.One2many('mrp.routing.workcenter', 'workcenter_id', "Routing Lines")
|
|
|
|
order_ids = fields.One2many('mrp.workorder', 'workcenter_id', "Orders")
|
|
workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count')
|
|
workorder_ready_count = fields.Integer('# Read Work Orders', compute='_compute_workorder_count')
|
|
workorder_progress_count = fields.Integer('Total Running Orders', compute='_compute_workorder_count')
|
|
workorder_pending_count = fields.Integer('Total Running Orders', compute='_compute_workorder_count')
|
|
workorder_late_count = fields.Integer('Total Late Orders', compute='_compute_workorder_count')
|
|
|
|
time_ids = fields.One2many('mrp.workcenter.productivity', 'workcenter_id', 'Time Logs')
|
|
working_state = fields.Selection([
|
|
('normal', 'Normal'),
|
|
('blocked', 'Blocked'),
|
|
('done', 'In Progress')], 'Status', compute="_compute_working_state", store=True)
|
|
blocked_time = fields.Float(
|
|
'Blocked Time', compute='_compute_blocked_time',
|
|
help='Blocked hour(s) over the last month', digits=(16, 2))
|
|
productive_time = fields.Float(
|
|
'Productive Time', compute='_compute_productive_time',
|
|
help='Productive hour(s) over the last month', digits=(16, 2))
|
|
oee = fields.Float(compute='_compute_oee', help='Overall Equipment Effectiveness, based on the last month')
|
|
oee_target = fields.Float(string='OEE Target', help="OEE Target in percentage", default=90)
|
|
performance = fields.Integer('Performance', compute='_compute_performance', help='Performance over the last month')
|
|
workcenter_load = fields.Float('Work Center Load', compute='_compute_workorder_count')
|
|
|
|
@api.depends('order_ids.duration_expected', 'order_ids.workcenter_id', 'order_ids.state', 'order_ids.date_planned_start')
|
|
def _compute_workorder_count(self):
|
|
MrpWorkorder = self.env['mrp.workorder']
|
|
result = {wid: {} for wid in self.ids}
|
|
result_duration_expected = {wid: 0 for wid in self.ids}
|
|
#Count Late Workorder
|
|
data = MrpWorkorder.read_group([('workcenter_id', 'in', self.ids), ('state', 'in', ('pending', 'ready')), ('date_planned_start', '<', datetime.datetime.now().strftime('%Y-%m-%d'))], ['workcenter_id'], ['workcenter_id'])
|
|
count_data = dict((item['workcenter_id'][0], item['workcenter_id_count']) for item in data)
|
|
#Count All, Pending, Ready, Progress Workorder
|
|
res = MrpWorkorder.read_group(
|
|
[('workcenter_id', 'in', self.ids)],
|
|
['workcenter_id', 'state', 'duration_expected'], ['workcenter_id', 'state'],
|
|
lazy=False)
|
|
for res_group in res:
|
|
result[res_group['workcenter_id'][0]][res_group['state']] = res_group['__count']
|
|
if res_group['state'] in ('pending', 'ready', 'progress'):
|
|
result_duration_expected[res_group['workcenter_id'][0]] += res_group['duration_expected']
|
|
for workcenter in self:
|
|
workcenter.workorder_count = sum(count for state, count in result[workcenter.id].items() if state not in ('done', 'cancel'))
|
|
workcenter.workorder_pending_count = result[workcenter.id].get('pending', 0)
|
|
workcenter.workcenter_load = result_duration_expected[workcenter.id]
|
|
workcenter.workorder_ready_count = result[workcenter.id].get('ready', 0)
|
|
workcenter.workorder_progress_count = result[workcenter.id].get('progress', 0)
|
|
workcenter.workorder_late_count = count_data.get(workcenter.id, 0)
|
|
|
|
@api.multi
|
|
@api.depends('time_ids', 'time_ids.date_end', 'time_ids.loss_type')
|
|
def _compute_working_state(self):
|
|
for workcenter in self:
|
|
# We search for a productivity line associated to this workcenter having no `date_end`.
|
|
# If we do not find one, the workcenter is not currently being used. If we find one, according
|
|
# to its `type_loss`, the workcenter is either being used or blocked.
|
|
time_log = self.env['mrp.workcenter.productivity'].search([
|
|
('workcenter_id', '=', workcenter.id),
|
|
('date_end', '=', False)
|
|
], limit=1)
|
|
if not time_log:
|
|
# the workcenter is not being used
|
|
workcenter.working_state = 'normal'
|
|
elif time_log.loss_type in ('productive', 'performance'):
|
|
# the productivity line has a `loss_type` that means the workcenter is being used
|
|
workcenter.working_state = 'done'
|
|
else:
|
|
# the workcenter is blocked
|
|
workcenter.working_state = 'blocked'
|
|
|
|
@api.multi
|
|
def _compute_blocked_time(self):
|
|
# TDE FIXME: productivity loss type should be only losses, probably count other time logs differently ??
|
|
data = self.env['mrp.workcenter.productivity'].read_group([
|
|
('date_start', '>=', fields.Datetime.to_string(datetime.datetime.now() - relativedelta.relativedelta(months=1))),
|
|
('workcenter_id', 'in', self.ids),
|
|
('date_end', '!=', False),
|
|
('loss_type', '!=', 'productive')],
|
|
['duration', 'workcenter_id'], ['workcenter_id'], lazy=False)
|
|
count_data = dict((item['workcenter_id'][0], item['duration']) for item in data)
|
|
for workcenter in self:
|
|
workcenter.blocked_time = count_data.get(workcenter.id, 0.0) / 60.0
|
|
|
|
@api.multi
|
|
def _compute_productive_time(self):
|
|
# TDE FIXME: productivity loss type should be only losses, probably count other time logs differently
|
|
data = self.env['mrp.workcenter.productivity'].read_group([
|
|
('date_start', '>=', fields.Datetime.to_string(datetime.datetime.now() - relativedelta.relativedelta(months=1))),
|
|
('workcenter_id', 'in', self.ids),
|
|
('date_end', '!=', False),
|
|
('loss_type', '=', 'productive')],
|
|
['duration', 'workcenter_id'], ['workcenter_id'], lazy=False)
|
|
count_data = dict((item['workcenter_id'][0], item['duration']) for item in data)
|
|
for workcenter in self:
|
|
workcenter.productive_time = count_data.get(workcenter.id, 0.0) / 60.0
|
|
|
|
@api.depends('blocked_time', 'productive_time')
|
|
def _compute_oee(self):
|
|
for order in self:
|
|
if order.productive_time:
|
|
order.oee = round(order.productive_time * 100.0 / (order.productive_time + order.blocked_time), 2)
|
|
else:
|
|
order.oee = 0.0
|
|
|
|
@api.multi
|
|
def _compute_performance(self):
|
|
wo_data = self.env['mrp.workorder'].read_group([
|
|
('date_start', '>=', fields.Datetime.to_string(datetime.datetime.now() - relativedelta.relativedelta(months=1))),
|
|
('workcenter_id', 'in', self.ids),
|
|
('state', '=', 'done')], ['duration_expected', 'workcenter_id', 'duration'], ['workcenter_id'], lazy=False)
|
|
duration_expected = dict((data['workcenter_id'][0], data['duration_expected']) for data in wo_data)
|
|
duration = dict((data['workcenter_id'][0], data['duration']) for data in wo_data)
|
|
for workcenter in self:
|
|
if duration.get(workcenter.id):
|
|
workcenter.performance = 100 * duration_expected.get(workcenter.id, 0.0) / duration[workcenter.id]
|
|
else:
|
|
workcenter.performance = 0.0
|
|
|
|
@api.multi
|
|
@api.constrains('capacity')
|
|
def _check_capacity(self):
|
|
if any(workcenter.capacity <= 0.0 for workcenter in self):
|
|
raise exceptions.UserError(_('The capacity must be strictly positive.'))
|
|
|
|
@api.multi
|
|
def unblock(self):
|
|
self.ensure_one()
|
|
if self.working_state != 'blocked':
|
|
raise exceptions.UserError(_("It has been unblocked already. "))
|
|
times = self.env['mrp.workcenter.productivity'].search([('workcenter_id', '=', self.id), ('date_end', '=', False)])
|
|
times.write({'date_end': fields.Datetime.now()})
|
|
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
|
|
|
|
|
class MrpWorkcenterProductivityLoss(models.Model):
|
|
_name = "mrp.workcenter.productivity.loss"
|
|
_description = "TPM Big Losses"
|
|
_order = "sequence, id"
|
|
|
|
name = fields.Char('Reason', required=True)
|
|
sequence = fields.Integer('Sequence', default=1)
|
|
manual = fields.Boolean('Is a Blocking Reason', default=True)
|
|
loss_type = fields.Selection([
|
|
('availability', 'Availability'),
|
|
('performance', 'Performance'),
|
|
('quality', 'Quality'),
|
|
('productive', 'Productive')], "Effectiveness Category",
|
|
default='availability', required=True)
|
|
|
|
|
|
class MrpWorkcenterProductivity(models.Model):
|
|
_name = "mrp.workcenter.productivity"
|
|
_description = "Workcenter Productivity Log"
|
|
_order = "id desc"
|
|
_rec_name = "loss_id"
|
|
|
|
workcenter_id = fields.Many2one('mrp.workcenter', "Work Center", required=True)
|
|
workorder_id = fields.Many2one('mrp.workorder', 'Work Order')
|
|
user_id = fields.Many2one(
|
|
'res.users', "User",
|
|
default=lambda self: self.env.uid)
|
|
loss_id = fields.Many2one(
|
|
'mrp.workcenter.productivity.loss', "Loss Reason",
|
|
ondelete='restrict', required=True)
|
|
loss_type = fields.Selection(
|
|
"Effectiveness", related='loss_id.loss_type', store=True)
|
|
description = fields.Text('Description')
|
|
date_start = fields.Datetime('Start Date', default=fields.Datetime.now, required=True)
|
|
date_end = fields.Datetime('End Date')
|
|
duration = fields.Float('Duration', compute='_compute_duration', store=True)
|
|
|
|
@api.depends('date_end', 'date_start')
|
|
def _compute_duration(self):
|
|
for blocktime in self:
|
|
if blocktime.date_end:
|
|
diff = fields.Datetime.from_string(blocktime.date_end) - fields.Datetime.from_string(blocktime.date_start)
|
|
blocktime.duration = round(diff.total_seconds() / 60.0, 2)
|
|
else:
|
|
blocktime.duration = 0.0
|
|
|
|
@api.multi
|
|
def button_block(self):
|
|
self.ensure_one()
|
|
self.workcenter_id.order_ids.end_all()
|
|
return {'type': 'ir.actions.client', 'tag': 'reload'}
|