summaryrefslogtreecommitdiff
path: root/addons/mrp/models/mrp_workcenter.py
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mrp/models/mrp_workcenter.py
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mrp/models/mrp_workcenter.py')
-rw-r--r--addons/mrp/models/mrp_workcenter.py346
1 files changed, 346 insertions, 0 deletions
diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py
new file mode 100644
index 00000000..190581ee
--- /dev/null
+++ b/addons/mrp/models/mrp_workcenter.py
@@ -0,0 +1,346 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from dateutil import relativedelta
+from datetime import timedelta
+from functools import partial
+import datetime
+from pytz import timezone
+
+from odoo import api, exceptions, fields, models, _
+from odoo.exceptions import ValidationError
+from odoo.addons.resource.models.resource import make_aware, Intervals
+from odoo.tools.float_utils import float_compare
+
+
+class MrpWorkcenter(models.Model):
+ _name = 'mrp.workcenter'
+ _description = 'Work Center'
+ _order = "sequence, id"
+ _inherit = ['resource.mixin']
+ _check_company_auto = True
+
+ # resource
+ name = fields.Char('Work Center', related='resource_id.name', store=True, readonly=False)
+ time_efficiency = fields.Float('Time Efficiency', related='resource_id.time_efficiency', default=100, store=True, readonly=False)
+ active = fields.Boolean('Active', related='resource_id.active', default=True, store=True, readonly=False)
+
+ code = fields.Char('Code', copy=False)
+ note = fields.Text(
+ 'Description',
+ help="Description of the Work Center.")
+ capacity = fields.Float(
+ 'Capacity', default=1.0,
+ help="Number of pieces that can be produced in parallel. In case the work center has a capacity of 5 and you have to produce 10 units on your work order, the usual operation time will be multiplied by 2.")
+ sequence = fields.Integer(
+ 'Sequence', default=1, required=True,
+ help="Gives the sequence order when displaying a list of work centers.")
+ color = fields.Integer('Color')
+ costs_hour = fields.Float(string='Cost per hour', help='Specify cost of work center per hour.', default=0.0)
+ 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.")
+ 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 Pending 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')], 'Workcenter Status', compute="_compute_working_state", store=True)
+ blocked_time = fields.Float(
+ 'Blocked Time', compute='_compute_blocked_time',
+ help='Blocked hours over the last month', digits=(16, 2))
+ productive_time = fields.Float(
+ 'Productive Time', compute='_compute_productive_time',
+ help='Productive hours 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="Overall Effective Efficiency 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')
+ alternative_workcenter_ids = fields.Many2many(
+ 'mrp.workcenter',
+ 'mrp_workcenter_alternative_rel',
+ 'workcenter_id',
+ 'alternative_workcenter_id',
+ domain="[('id', '!=', id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]",
+ string="Alternative Workcenters", check_company=True,
+ help="Alternative workcenters that can be substituted to this one in order to dispatch production"
+ )
+
+ @api.constrains('alternative_workcenter_ids')
+ def _check_alternative_workcenter(self):
+ if self in self.alternative_workcenter_ids:
+ raise ValidationError(_("A workcenter cannot be an alternative of itself"))
+
+ @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.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'
+
+ 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
+
+ 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
+
+ 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.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.'))
+
+ def unblock(self):
+ self.ensure_one()
+ if self.working_state != 'blocked':
+ raise exceptions.UserError(_("It has already been unblocked."))
+ 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'}
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ # resource_type is 'human' by default. As we are not living in
+ # /r/latestagecapitalism, workcenters are 'material'
+ records = super(MrpWorkcenter, self.with_context(default_resource_type='material')).create(vals_list)
+ return records
+
+ def write(self, vals):
+ if 'company_id' in vals:
+ self.resource_id.company_id = vals['company_id']
+ return super(MrpWorkcenter, self).write(vals)
+
+ def action_work_order(self):
+ action = self.env["ir.actions.actions"]._for_xml_id("mrp.action_work_orders")
+ return action
+
+ def _get_unavailability_intervals(self, start_datetime, end_datetime):
+ """Get the unavailabilities intervals for the workcenters in `self`.
+
+ Return the list of unavailabilities (a tuple of datetimes) indexed
+ by workcenter id.
+
+ :param start_datetime: filter unavailability with only slots after this start_datetime
+ :param end_datetime: filter unavailability with only slots before this end_datetime
+ :rtype: dict
+ """
+ unavailability_ressources = self.resource_id._get_unavailable_intervals(start_datetime, end_datetime)
+ return {wc.id: unavailability_ressources.get(wc.resource_id.id, []) for wc in self}
+
+ def _get_first_available_slot(self, start_datetime, duration):
+ """Get the first available interval for the workcenter in `self`.
+
+ The available interval is disjoinct with all other workorders planned on this workcenter, but
+ can overlap the time-off of the related calendar (inverse of the working hours).
+ Return the first available interval (start datetime, end datetime) or,
+ if there is none before 700 days, a tuple error (False, 'error message').
+
+ :param start_datetime: begin the search at this datetime
+ :param duration: minutes needed to make the workorder (float)
+ :rtype: tuple
+ """
+ self.ensure_one()
+ start_datetime, revert = make_aware(start_datetime)
+
+ get_available_intervals = partial(self.resource_calendar_id._work_intervals, domain=[('time_type', 'in', ['other', 'leave'])], resource=self.resource_id, tz=timezone(self.resource_calendar_id.tz))
+ get_workorder_intervals = partial(self.resource_calendar_id._leave_intervals, domain=[('time_type', '=', 'other')], resource=self.resource_id, tz=timezone(self.resource_calendar_id.tz))
+
+ remaining = duration
+ start_interval = start_datetime
+ delta = timedelta(days=14)
+
+ for n in range(50): # 50 * 14 = 700 days in advance (hardcoded)
+ dt = start_datetime + delta * n
+ available_intervals = get_available_intervals(dt, dt + delta)
+ workorder_intervals = get_workorder_intervals(dt, dt + delta)
+ for start, stop, dummy in available_intervals:
+ interval_minutes = (stop - start).total_seconds() / 60
+ # If the remaining minutes has never decrease update start_interval
+ if remaining == duration:
+ start_interval = start
+ # If there is a overlap between the possible available interval and a others WO
+ if Intervals([(start_interval, start + timedelta(minutes=min(remaining, interval_minutes)), dummy)]) & workorder_intervals:
+ remaining = duration
+ start_interval = start
+ elif float_compare(interval_minutes, remaining, precision_digits=3) >= 0:
+ return revert(start_interval), revert(start + timedelta(minutes=remaining))
+ # Decrease a part of the remaining duration
+ remaining -= interval_minutes
+ return False, 'Not available slot 700 days after the planned start'
+
+
+class MrpWorkcenterProductivityLossType(models.Model):
+ _name = "mrp.workcenter.productivity.loss.type"
+ _description = 'MRP Workorder productivity losses'
+ _rec_name = 'loss_type'
+
+ @api.depends('loss_type')
+ def name_get(self):
+ """ As 'category' field in form view is a Many2one, its value will be in
+ lower case. In order to display its value capitalized 'name_get' is
+ overrided.
+ """
+ result = []
+ for rec in self:
+ result.append((rec.id, rec.loss_type.title()))
+ return result
+
+ loss_type = fields.Selection([
+ ('availability', 'Availability'),
+ ('performance', 'Performance'),
+ ('quality', 'Quality'),
+ ('productive', 'Productive')], string='Category', default='availability', required=True)
+
+
+class MrpWorkcenterProductivityLoss(models.Model):
+ _name = "mrp.workcenter.productivity.loss"
+ _description = "Workcenter Productivity Losses"
+ _order = "sequence, id"
+
+ name = fields.Char('Blocking Reason', required=True)
+ sequence = fields.Integer('Sequence', default=1)
+ manual = fields.Boolean('Is a Blocking Reason', default=True)
+ loss_id = fields.Many2one('mrp.workcenter.productivity.loss.type', domain=([('loss_type', 'in', ['quality', 'availability'])]), string='Category')
+ loss_type = fields.Selection(string='Effectiveness Category', related='loss_id.loss_type', store=True, readonly=False)
+
+
+class MrpWorkcenterProductivity(models.Model):
+ _name = "mrp.workcenter.productivity"
+ _description = "Workcenter Productivity Log"
+ _order = "id desc"
+ _rec_name = "loss_id"
+ _check_company_auto = True
+
+ def _get_default_company_id(self):
+ company_id = False
+ if self.env.context.get('default_company_id'):
+ company_id = self.env.context['default_company_id']
+ if not company_id and self.env.context.get('default_workorder_id'):
+ workorder = self.env['mrp.workorder'].browse(self.env.context['default_workorder_id'])
+ company_id = workorder.company_id
+ if not company_id and self.env.context.get('default_workcenter_id'):
+ workcenter = self.env['mrp.workcenter'].browse(self.env.context['default_workcenter_id'])
+ company_id = workcenter.company_id
+ if not company_id:
+ company_id = self.env.company
+ return company_id
+
+ production_id = fields.Many2one('mrp.production', string='Manufacturing Order', related='workorder_id.production_id', readonly='True')
+ workcenter_id = fields.Many2one('mrp.workcenter', "Work Center", required=True, check_company=True)
+ company_id = fields.Many2one(
+ 'res.company', required=True, index=True,
+ default=lambda self: self._get_default_company_id())
+ workorder_id = fields.Many2one('mrp.workorder', 'Work Order', check_company=True)
+ 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(
+ string="Effectiveness", related='loss_id.loss_type', store=True, readonly=False)
+ 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_start and blocktime.date_end:
+ d1 = fields.Datetime.from_string(blocktime.date_start)
+ d2 = fields.Datetime.from_string(blocktime.date_end)
+ diff = d2 - d1
+ if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.resource_calendar_id:
+ r = blocktime.workcenter_id._get_work_days_data_batch(d1, d2)[blocktime.workcenter_id.id]['hours']
+ blocktime.duration = round(r * 60, 2)
+ else:
+ blocktime.duration = round(diff.total_seconds() / 60.0, 2)
+ else:
+ blocktime.duration = 0.0
+
+ def button_block(self):
+ self.ensure_one()
+ self.workcenter_id.order_ids.end_all()