Files
enterprise-addons/mrp_workorder/models/mrp_workorder.py
T

914 lines
45 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from bisect import bisect_left
from collections import defaultdict
from datetime import datetime
from dateutil.relativedelta import relativedelta
from pytz import utc
from odoo import Command, api, fields, models, _
from odoo.addons.web.controllers.utils import clean_action
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_is_zero
from odoo.tools.intervals import Intervals
from odoo.tools.date_utils import sum_intervals
from odoo.http import request
class MrpWorkorder(models.Model):
_name = 'mrp.workorder'
_inherit = ['hr.mixin', 'mrp.workorder', 'barcodes.barcode_events_mixin']
quality_point_ids = fields.Many2many('quality.point', compute='_compute_quality_point_ids', store=True)
quality_point_count = fields.Integer('Steps', compute='_compute_quality_point_count')
check_ids = fields.One2many('quality.check', 'workorder_id')
finished_product_check_ids = fields.Many2many('quality.check', compute='_compute_finished_product_check_ids')
done_check_ids = fields.Many2many('quality.check', compute='_compute_done_check_ids')
quality_check_todo = fields.Boolean(compute='_compute_check')
quality_check_fail = fields.Boolean(compute='_compute_check')
quality_alert_ids = fields.One2many('quality.alert', 'workorder_id')
quality_alert_count = fields.Integer(compute="_compute_quality_alert_count")
current_quality_check_id = fields.Many2one(
'quality.check', "Current Quality Check", check_company=True)
# QC-related fields
allow_producing_quantity_change = fields.Boolean('Allow Changes to Producing Quantity', default=True)
is_last_lot = fields.Boolean('Is Last lot', compute='_compute_is_last_lot')
is_first_started_wo = fields.Boolean('Is The first Work Order', compute='_compute_is_last_unfinished_wo')
is_last_unfinished_wo = fields.Boolean('Is Last Work Order To Process', compute='_compute_is_last_unfinished_wo', store=False)
move_id = fields.Many2one(related='current_quality_check_id.move_id', readonly=False)
move_line_ids = fields.One2many(related='move_id.move_line_ids')
quality_state = fields.Selection(related='current_quality_check_id.quality_state', string="Quality State", readonly=False)
test_type_id = fields.Many2one('quality.point.test_type', 'Test Type', related='current_quality_check_id.test_type_id')
test_type = fields.Char(related='test_type_id.technical_name')
user_id = fields.Many2one(related='current_quality_check_id.user_id', readonly=False)
worksheet_page = fields.Integer('Worksheet page')
picture = fields.Binary(related='current_quality_check_id.picture', readonly=False)
product_description_variants = fields.Char(related='production_id.product_description_variants')
# used to display the connected employee that will start a workorder on the tablet view
employee_id = fields.Many2one('hr.employee', string="Employee", compute='_compute_employee_id')
employee_name = fields.Char(compute='_compute_employee_id')
# employees that started working on the wo
employee_ids = fields.Many2many('hr.employee', string='Working employees', copy=False)
# employees assigned to the wo
employee_assigned_ids = fields.Many2many('hr.employee', 'mrp_workorder_employee_assigned',
'workorder_id', 'employee_id', string='Assigned',
domain="[('company_id', 'in', allowed_company_ids)]")
# employees connected
connected_employee_ids = fields.Many2many('hr.employee', search='search_is_assigned_to_connected', store=False)
# list of employees allowed to work on the workcenter
allowed_employees = fields.Many2many(related='workcenter_id.employee_ids')
# True if all employees are allowed on that workcenter
all_employees_allowed = fields.Boolean(compute='_all_employees_allowed')
# Technical field to store the estimated hourly cost of employee at time of work order completion (i.e. to keep a consistent cost).
employee_costs_hour = fields.Float(string='Employee Cost per hour', default=0.0)
@api.depends('operation_id')
def _compute_quality_point_ids(self):
for workorder in self:
quality_points = workorder.operation_id.quality_point_ids
quality_points = quality_points.filtered(lambda qp: (not qp.product_ids or workorder.production_id.product_id in qp.product_ids) and (qp.company_id == workorder.company_id))
workorder.quality_point_ids = quality_points
@api.depends('operation_id')
def _compute_quality_point_count(self):
for workorder in self:
quality_point = workorder.operation_id.quality_point_ids
workorder.quality_point_count = len(quality_point)
@api.depends('qty_producing', 'qty_remaining')
def _compute_is_last_lot(self):
for wo in self:
precision = wo.production_id.product_uom_id.rounding
wo.is_last_lot = float_compare(wo.qty_producing, wo.qty_remaining, precision_rounding=precision) >= 0
@api.depends('production_id.workorder_ids')
def _compute_is_last_unfinished_wo(self):
for wo in self:
wo.is_first_started_wo = all(wo.state != 'done' for wo in (wo.production_id.workorder_ids - wo))
other_wos = wo.production_id.workorder_ids - wo
other_states = other_wos.mapped(lambda w: w.state in ['done', 'cancel'])
wo.is_last_unfinished_wo = all(other_states)
@api.depends('check_ids')
def _compute_finished_product_check_ids(self):
for wo in self:
wo.finished_product_check_ids = wo.check_ids.filtered(lambda c: c.finished_product_sequence == wo.qty_produced)
@api.depends('check_ids.quality_state')
def _compute_done_check_ids(self):
for wo in self:
wo.done_check_ids = wo.check_ids.filtered(lambda c: c.quality_state != 'none')
def unlink(self):
self.check_ids.sudo().unlink()
return super().unlink()
def action_back(self):
self.ensure_one()
if self._should_be_pending():
self.button_pending()
domain = [('state', 'not in', ['done', 'cancel', 'blocked'])]
if self.env.context.get('from_manufacturing_order'):
# from workorder on MO
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.mrp_workorder_action_tablet")
action['domain'] = domain
action['context'] = literal_eval(action['context']) | {
'no_breadcrumbs': True,
'search_default_production_id': self.production_id.id,
'search_default_workcenter_id': False,
'search_default_progress': False,
'search_default_ready': False,
'from_manufacturing_order': True,
}
elif self.env.context.get('from_production_order'):
# from workorder list view
action = self.env["ir.actions.actions"]._for_xml_id("mrp.mrp_workorder_todo")
action['target'] = 'main'
action['context'] = dict(literal_eval(action['context']), no_breadcrumbs=True)
elif self.env.context.get('mrp_display'):
return
else:
# from workcenter kanban view
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.mrp_workorder_action_tablet")
action['domain'] = domain
action['context'] = literal_eval(action['context'].replace('active_id', str(self.id))) | {
'no_breadcrumbs': True,
'search_default_workcenter_id': self.workcenter_id.id,
}
if self.employee_id:
action['context']['employee_id'] = self.employee_id.id
action['context']['employee_name'] = self.employee_id.name
if self.employee_ids:
action['context']['employee_ids'] = self.employee_ids
return clean_action(action, self.env)
def action_cancel(self):
self.mapped('check_ids').filtered(lambda c: c.quality_state == 'none').sudo().unlink()
return super().action_cancel()
def action_generate_serial(self):
self.ensure_one()
return self.production_id.action_generate_serial(self)
def _change_quality_check(self, position):
"""Change the quality check currently set on the workorder `self`.
The workorder points to a check. A check belongs to a chain.
This method allows to change the selected check by moving on the checks
chain according to `position`.
:param position: Where we need to change the cursor on the check chain
:type position: string
"""
self.ensure_one()
assert position in ['first', 'next', 'previous', 'last']
checks_to_consider = self.check_ids.filtered(lambda c: c.quality_state == 'none')
if position == 'first':
check = checks_to_consider.filtered(lambda check: not check.previous_check_id)
elif position == 'next':
check = self.current_quality_check_id.next_check_id
if not check:
check = checks_to_consider[:1]
elif check.quality_state != 'none':
self.current_quality_check_id = check
return self._change_quality_check(position='next')
elif position == 'previous':
check = self.current_quality_check_id.previous_check_id
else:
check = checks_to_consider.filtered(lambda check: not check.next_check_id)
self.write({
'allow_producing_quantity_change':
not check.previous_check_id.filtered(lambda c: c.quality_state != 'fail')
and all(c.quality_state != 'fail' for c in checks_to_consider)
and self.is_first_started_wo,
'current_quality_check_id': check.id,
})
return check.id
def action_menu(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp.workorder',
'views': [[self.env.ref('mrp_workorder.mrp_workorder_view_form_tablet_menu').id, 'form']],
'name': _('Menu'),
'target': 'new',
'res_id': self.id,
}
def action_add_component(self):
self.ensure_one()
return self.production_id.with_context(workorder_id=self.id).action_add_component()
def action_add_byproduct(self):
self.ensure_one()
return self.production_id.with_context(workorder_id=self.id).action_add_byproduct()
def action_add_workorder(self):
self.ensure_one()
default_blocking_wo_id = self.id
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp_production.additional.workorder',
'views': [[self.env.ref('mrp_workorder.view_mrp_production_additional_workorder_wizard').id, 'form']],
'name': _('Add Work Order'),
'target': 'new',
'context': {
'default_production_id': self.production_id.id,
'default_blocked_by_workorder_id': default_blocking_wo_id,
}
}
def button_start(self, raise_on_invalid_state=False, bypass=False):
skip_employee_check = bypass or (not request and not self.env.user.employee_id)
main_employee = False
if not skip_employee_check:
if not self.env.context.get('mrp_display'):
main_employee = self.env.user.employee_id.id
if not self.env.user.employee_id:
raise UserError(_("You need to link this user to an employee of this company to process the work order"))
else:
connected_employees = self.env['hr.employee'].get_employees_connected()
if len(connected_employees) == 0:
raise UserError(_("You need to log in to process this work order."))
main_employee = self.env['hr.employee'].get_session_owner()
if not main_employee:
raise UserError(_("There is no session chief. Please log in."))
if any(main_employee not in [emp.id for emp in wo.allowed_employees] and not wo.all_employees_allowed for wo in self):
raise UserError(_("You are not allowed to work on the workorder"))
res = super().button_start(raise_on_invalid_state=raise_on_invalid_state)
if main_employee:
main_employee = self.env['hr.employee'].browse(main_employee)
for wo in self:
if (len(wo.allowed_employees) == 0 or main_employee in wo.allowed_employees) and wo.state not in ('done', 'cancel'):
wo.start_employee(main_employee.id)
wo.employee_ids |= main_employee
wo.employee_assigned_ids |= main_employee
return res
def button_finish(self):
""" When using the Done button of the simplified view, validate directly some types of quality checks
"""
for workorder in self:
if workorder.state in ('done', 'cancel'):
continue
workorder.employee_costs_hour = workorder.workcenter_id.employee_costs_hour
self.verify_quality_checks()
return super().button_finish()
def verify_quality_checks(self):
for check in self.check_ids:
if check.quality_state in ['pass', 'fail']:
continue
if check.test_type in ['register_consumed_materials', 'register_byproducts', 'instructions']:
check.quality_state = 'pass'
else:
raise UserError(_("You need to complete Quality Checks using the Shop Floor before marking Work Order as Done."))
def action_propose_change(self, change_type, check_id):
change_type_to_title = {'update_step': 'Update Instructions', 'remove_step': 'Remove Step', 'set_picture': 'Change Picture'}
return {
'type': 'ir.actions.act_window',
'res_model': 'propose.change',
'views': [[self.env.ref('mrp_workorder.view_propose_change_wizard').id, 'form']],
'name': change_type_to_title[change_type],
'target': 'new',
'context': {
'default_workorder_id': self.id,
'default_step_id': check_id,
'default_change_type': change_type,
}
}
def action_add_step(self):
self.ensure_one()
if self.current_quality_check_id:
team = self.current_quality_check_id.team_id
else:
team = self.env['quality.alert.team'].search(['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], limit=1)
return {
'type': 'ir.actions.act_window',
'res_model': 'quality.check',
'views': [[self.env.ref('mrp_workorder.add_quality_check_from_tablet').id, 'form']],
'name': _('Add a Step'),
'target': 'new',
'context': {
'default_test_type_id': self.env.ref('quality.test_type_instructions').id,
'default_workorder_id': self.id,
'default_production_id': self.production_id.id,
'default_product_id': self.product_id.id,
'default_team_id': team.id,
}
}
def action_open_mes(self):
action = self.env['ir.actions.actions']._for_xml_id('mrp_workorder.action_mrp_display')
action['context'] = literal_eval(action['context']) | {
'workcenter_id': self.workcenter_id.id,
'search_default_progress': False,
'search_default_ready': False,
'search_default_name': self.production_id.name,
'shouldHideNewWorkcenterButton': True,
}
del action['context']['search_default_filter_ready'] # Remove MO Ready
return action
def action_log_note(self):
return self.production_id.action_log_note()
def _compute_check(self):
for workorder in self:
todo = False
fail = False
for check in workorder.check_ids:
if check.quality_state == 'none':
todo = True
elif check.quality_state == 'fail':
fail = True
if fail and todo:
break
workorder.quality_check_fail = fail
workorder.quality_check_todo = todo
def _compute_quality_alert_count(self):
for workorder in self:
workorder.quality_alert_count = len(workorder.quality_alert_ids)
def _create_checks(self):
for wo in self:
# Track components which have a control point
processed_move = self.env['stock.move']
production = wo.production_id
move_raw_ids = wo.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
move_finished_ids = wo.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.product_id != wo.production_id.product_id)
previous_check = self.env['quality.check']
for point in wo.quality_point_ids:
# Check if we need a quality control for this point
if point.check_execute_now():
moves = self.env['stock.move']
values = {
'production_id': production.id,
'workorder_id': wo.id,
'point_id': point.id,
'team_id': point.team_id.id,
'company_id': wo.company_id.id,
'product_id': production.product_id.id,
# Two steps are from the same production
# if and only if the produced quantities at the time they were created are equal.
'finished_product_sequence': wo.qty_produced,
'previous_check_id': previous_check.id,
'worksheet_document': point.worksheet_document,
}
if point.test_type == 'register_byproducts':
moves = move_finished_ids.filtered(lambda m: m.product_id == point.component_id)
if not moves:
moves = production.move_finished_ids.filtered(lambda m: not m.operation_id and m.product_id == point.component_id)
elif point.test_type == 'register_consumed_materials':
moves = move_raw_ids.filtered(lambda m: m.product_id == point.component_id)
if not moves:
moves = production.move_raw_ids.filtered(lambda m: not m.operation_id and m.product_id == point.component_id)
else:
check = self.env['quality.check'].create(values)
previous_check.next_check_id = check
previous_check = check
# Create 'register ...' checks
for move in moves:
check_vals = values.copy()
check_vals.update(wo._defaults_from_move(move))
# Create quality check and link it to the chain
check_vals.update({'previous_check_id': previous_check.id})
check = self.env['quality.check'].create(check_vals)
previous_check.next_check_id = check
previous_check = check
processed_move |= moves
# Set default quality_check
wo._change_quality_check(position='first')
def _get_byproduct_move_to_update(self):
moves = super()._get_byproduct_move_to_update()
return moves.filtered(lambda m: m.product_id.tracking == 'none')
def pre_record_production(self):
self.ensure_one()
self._check_company()
if any(x.quality_state == 'none' for x in self.check_ids if x.test_type != 'instructions'):
raise UserError(_('You still need to do the quality checks!'))
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
def record_production(self):
if not self:
return True
self.pre_record_production()
backorder = False
# Trigger the backorder process if we produce less than expected
if float_compare(self.qty_producing, self.qty_remaining, precision_rounding=self.product_uom_id.rounding) == -1 and self.is_first_started_wo:
match self.production_id.picking_type_id.create_backorder:
case "ask":
return self.production_id.with_context(workorder_id_to_finish=self.id)._action_generate_backorder_wizard(self.production_id)
case "always":
backorder = self.production_id._split_productions({self.production_id: [self.qty_producing, self.qty_remaining - self.qty_producing]})[1:]
for workorder in backorder.workorder_ids:
if not self.env.context.get('no_start_next', False):
workorder.qty_producing = workorder.qty_remaining
self.production_id.product_qty = self.qty_producing
else:
if self.operation_id:
backorder = (self.production_id.production_group_id.production_ids - self.production_id).filtered(
lambda p: p.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_id).state not in ('cancel', 'done')
)[:1]
else:
index = list(self.production_id.workorder_ids).index(self)
backorder = (self.production_id.production_group_id.production_ids - self.production_id).filtered(
lambda p: index < len(p.workorder_ids) and p.workorder_ids[index].state not in ('cancel', 'done')
)[:1]
return self.post_record_production(backorder)
def post_record_production(self, backorders=False):
self.button_finish()
if backorders:
for wo in (self.production_id | backorders).workorder_ids:
if wo.state in ('done', 'cancel'):
continue
wo.current_quality_check_id.update(wo._defaults_from_move(wo.move_id))
return self.action_back()
def _defaults_from_move(self, move):
self.ensure_one()
vals = {'move_id': move.id}
move_line_id = move.move_line_ids[:1]
if move_line_id:
vals.update({
'lot_ids': move_line_id.lot_id.ids,
})
return vals
# --------------------------
# Buttons from quality.check
# --------------------------
def open_tablet_view(self):
self.ensure_one()
self.env['hr.employee'].login_user_employee()
if self._should_start():
self.button_start()
action = self.env["ir.actions.actions"]._for_xml_id("mrp_workorder.tablet_client_action")
action['target'] = 'fullscreen'
action['res_id'] = self.id
action['context'] = {
'active_id': self.id,
'from_production_order': self.env.context.get('from_production_order'),
'from_manufacturing_order': self.env.context.get('from_manufacturing_order')
}
return action
def action_open_manufacturing_order(self):
no_start_next = self.env.context.get("no_start_next", True)
action = self.with_context(no_start_next=no_start_next).do_finish()
try:
with self.env.cr.savepoint():
res = self.production_id.button_mark_done()
if res is not True:
res['context'] = dict(res.get('context', {}), from_workorder=True)
return res
except (UserError, ValidationError) as e:
# log next activity on MO with error message
self.production_id.activity_schedule(
'mail.mail_activity_data_warning',
note=str(e),
summary=('The %s could not be closed') % (self.production_id.name),
user_id=self.env.user.id)
return {
'type': 'ir.actions.act_window',
'res_model': 'mrp.production',
'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']],
'res_id': self.production_id.id,
'target': 'main',
}
return action
def do_finish(self):
self.end_all()
if self.state != 'done':
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'productive')], limit=1)
if len(loss_id) < 1:
raise UserError(_("You need to define at least one productivity loss in the category 'Productive'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
action = self.record_production()
self._set_default_time_log(loss_id)
if action is not True:
return action
# workorder list view action should redirect to the same view instead of workorder kanban view when WO mark as done.
return self.action_back()
def get_summary_data(self):
self.ensure_one()
# show rainbow man only the first time
show_rainbow = any(not t.date_end for t in self.time_ids)
self.end_all()
if any(step.quality_state == 'none' for step in self.check_ids):
raise UserError(_('You still need to do the quality checks!'))
last30op = self.env['mrp.workorder'].search_read([
('operation_id', '=', self.operation_id.id),
('state', '=', 'done'),
('date_finished', '>', fields.Date.today() - relativedelta(days=30)),
], ['duration', 'qty_produced'])
last30op = sorted([item['duration'] / item['qty_produced'] for item in last30op])
# show rainbow man only for the best time in the last 30 days.
if last30op:
show_rainbow = show_rainbow and float_compare((self.duration / self.qty_producing), last30op[0], precision_digits=2) <= 0
score = 3
if self.check_ids:
passed_checks = len([check for check in self.check_ids if check.quality_state == 'pass'])
score = int(3.0 * passed_checks / len(self.check_ids))
return {
'duration': self.duration,
'position': bisect_left(last30op, self.duration), # which position regarded other workorders ranked by duration
'quality_score': score,
'show_rainbow': show_rainbow,
}
def _action_confirm(self):
res = super()._action_confirm()
self.filtered(lambda wo: wo.state != 'cancel' and not wo.check_ids)._create_checks()
return res
def _update_qty_producing(self, quantity):
if float_is_zero(quantity, precision_rounding=self.product_uom_id.rounding):
self.check_ids.unlink()
super()._update_qty_producing(quantity)
def _web_gantt_reschedule_is_record_candidate(self, start_date_field_name, stop_date_field_name):
return self.state not in ['progress', 'done', 'cancel'] \
and super()._web_gantt_reschedule_is_record_candidate(start_date_field_name, stop_date_field_name)
def _web_gantt_reschedule_compute_dates(self, date_candidate, search_forward, start_date_field_name, stop_date_field_name):
from_date, to_date = self.workcenter_id._get_first_available_slot(date_candidate, self.duration_expected, forward=search_forward, leaves_to_ignore=self.leave_id)
return [from_date, to_date]
def _web_gantt_reschedule_write_new_dates(self, new_start_date, new_stop_date, start_date_field_name, stop_date_field_name):
return super(MrpWorkorder, self.with_context(bypass_duration_calculation=True))._web_gantt_reschedule_write_new_dates(new_start_date, new_stop_date, start_date_field_name, stop_date_field_name)
def _web_gantt_progress_bar_workcenter_id(self, res_ids, start, stop):
self.env['mrp.workorder'].check_access('read')
workcenters = self.env['mrp.workcenter'].search([('id', 'in', res_ids)])
workorders = self.env['mrp.workorder'].search([
('workcenter_id', 'in', res_ids),
('state', 'not in', ['done', 'cancel']),
('date_start', '<=', stop.replace(tzinfo=None)),
('date_finished', '>=', start.replace(tzinfo=None)),
])
planned_hours = defaultdict(float)
workcenters_work_intervals, dummy = workcenters.resource_id._get_valid_work_intervals(start, stop)
for workorder in workorders:
max_start = max(start, utc.localize(workorder.date_start))
min_finished = min(stop, utc.localize(workorder.date_finished))
interval = Intervals([(max_start, min_finished, self.env['resource.calendar.attendance'])])
work_intervals = interval & workcenters_work_intervals[workorder.workcenter_id.resource_id.id]
planned_hours[workorder.workcenter_id] += sum_intervals(work_intervals)
work_hours = {
id: sum_intervals(work_intervals) for id, work_intervals in workcenters_work_intervals.items()
}
return {
workcenter.id: {
'value': planned_hours[workcenter],
'max_value': work_hours.get(workcenter.resource_id.id, 0.0),
}
for workcenter in workcenters
}
@api.model
def get_gantt_data(self, domain, groupby, read_specification, limit=None, offset=0, unavailability_fields=None, progress_bar_fields=None, start_date=None, stop_date=None, scale=None):
gantt_data = super(MrpWorkorder, self.with_context(prefix_product=True)).get_gantt_data(domain, groupby, read_specification, limit=limit, offset=offset, unavailability_fields=unavailability_fields, progress_bar_fields=progress_bar_fields, start_date=start_date, stop_date=stop_date, scale=scale)
if 'workcenter_id' not in gantt_data['unavailabilities']:
workcenter_ids = set()
if groupby and 'workcenter_id' in groupby:
for group in gantt_data['groups']:
res_id = group['workcenter_id'][0] if group['workcenter_id'] else False
workcenter_ids.add(res_id)
else:
for record in gantt_data['records']:
res_id = record['workcenter_id']['id'] if record.get('workcenter_id') else False
workcenter_ids.add(res_id)
start, stop = fields.Datetime.from_string(start_date), fields.Datetime.from_string(stop_date)
gantt_data['unavailabilities']['workcenter_id'] = self._gantt_unavailability('workcenter_id', workcenter_ids, start, stop, scale)
return gantt_data
@api.model
def _gantt_progress_bar(self, field, res_ids, start, stop):
start, stop = utc.localize(start), utc.localize(stop)
today = datetime.now(utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = max(start, today)
if field == 'workcenter_id':
return dict(
self._web_gantt_progress_bar_workcenter_id(res_ids, start, stop),
warning=_("This workcenter isn't expected to have open workorders during this period. Work hours :"),
)
raise NotImplementedError("This Progress Bar is not implemented.")
@api.model
def _gantt_unavailability(self, field, res_ids, start, stop, scale):
"""Get unavailabilities data to display in the Gantt view."""
if field != 'workcenter_id':
return super()._gantt_unavailability(field, res_ids, start, stop, scale)
workcenters = self.env['mrp.workcenter'].browse(res_ids)
unavailability_mapping = workcenters._get_unavailability_intervals(start, stop)
result = {}
for workcenter in workcenters:
result[workcenter.id] = [{'start': interval[0], 'stop': interval[1]} for interval in unavailability_mapping[workcenter.id]]
return result
@api.model
def get_shopfloor_limit(self):
return self.env['ir.config_parameter'].sudo().get_param("mrp_workorder.wo_shop_floor_maximum_card_count")
def _should_be_pending(self):
return self.is_user_working and self.working_state != 'blocked' and len(self.employee_ids.ids) == 0
def _should_start(self):
if self.working_state != 'blocked' and self.state in ('ready', 'blocked', 'progress'):
if self.env['hr.employee'].get_session_owner():
return True
else:
self.button_start(bypass=True)
return False
def _compute_duration(self):
wo_ids_without_employees = set()
for wo in self:
wo.duration = wo.get_duration()
wo.duration_unit = round(wo.duration / max(wo.qty_produced, 1), 2)
if wo.duration_expected:
wo.duration_percent = max(-2147483648, min(2147483647, 100 * (wo.duration_expected - wo.duration) / wo.duration_expected))
else:
wo.duration_percent = 0
return super(MrpWorkorder, self.env['mrp.workorder'].browse(wo_ids_without_employees))._compute_duration()
@api.depends('employee_ids')
def _compute_employee_id(self):
main_employee_connected = self.env['hr.employee'].get_session_owner()
self.employee_id = main_employee_connected
self.employee_name = self.env['hr.employee'].browse(main_employee_connected).name
def search_is_assigned_to_connected(self, operator, value):
# retrieving employees connected in the session
main_employee_connected = self.env['hr.employee'].get_session_owner()
# if no one is connected, all records are valid
if not main_employee_connected:
return []
search_query = self.env['mrp.workorder']._search([('employee_assigned_ids', '=', main_employee_connected)])
return [('id', operator, search_query)]
@api.depends('all_employees_allowed')
def _all_employees_allowed(self):
for wo in self:
wo.all_employees_allowed = len(wo.allowed_employees) == 0
def start_employee(self, employee_id):
self.ensure_one()
if employee_id in self.employee_ids.ids and any(not t.date_end for t in self.time_ids if t.employee_id.id == employee_id):
return
self.employee_ids = [Command.link(employee_id)]
time_data = self._prepare_timeline_vals(self.duration, fields.Datetime.now())
time_data['employee_id'] = employee_id
self.env['mrp.workcenter.productivity'].create(time_data)
self.state = "progress"
def stop_employee(self, employee_ids):
self.employee_ids = [Command.unlink(emp) for emp in employee_ids]
self.env['mrp.workcenter.productivity'].search([
('employee_id', 'in', employee_ids),
('workorder_id', 'in', self.ids),
('date_end', '=', False)
])._close()
def _should_start_timer(self):
""" Return True if the timer should start once the workorder is opened."""
self.ensure_one()
return False
def _intervals_duration(self, intervals):
""" Return the duration of the given intervals.
If intervals overlaps the duration is only counted once.
The timer could be share between several intervals. However it is not
an issue since the purpose is to make a difference between employee time and
blocking time.
:param list intervals: list of tuple (date_start, date_end, timer)
"""
if not intervals:
return 0.0
duration = 0
for date_start, date_stop, timer in Intervals(intervals):
duration += timer.loss_id._convert_to_duration(date_start, date_stop, timer.workcenter_id)
return duration
def get_duration(self):
self.ensure_one()
now = self.env.cr.now()
loss_type_times = defaultdict(lambda: self.env['mrp.workcenter.productivity'])
for time in self.time_ids:
loss_type_times[time.loss_id.loss_type] |= time
duration = 0
for times in loss_type_times.values():
duration += self._intervals_duration([(t.date_start or now, t.date_end or now, t) for t in times])
return duration
def get_working_duration(self):
self.ensure_one()
now = self.env.cr.now()
return self._intervals_duration([(t.date_start, now, t) for t in self.time_ids if not t.date_end])
def get_productive_duration(self):
self.ensure_one()
now = fields.Datetime.now()
productive_times = []
for time in self.time_ids:
if time.loss_id.loss_type == "productive":
productive_times.append(time)
duration = self._intervals_duration([(t.date_start, t.date_end or now, t) for t in productive_times])
return duration
def _cal_cost(self, date=False):
total_workcenter_cost = super()._cal_cost(date)
for wo in self:
if wo._should_estimate_cost():
total_workcenter_cost += (wo.duration_expected / 60) * (wo.employee_costs_hour or wo.workcenter_id.employee_costs_hour)
elif date:
total_workcenter_cost += sum(wo.time_ids.filtered(lambda t: t.date_end and t.date_end <= date).mapped('total_cost'))
else:
total_workcenter_cost += sum(wo.time_ids.mapped('total_cost'))
return total_workcenter_cost
def button_pending(self):
Employee = self.env['hr.employee']
# Determine current employee
if not self.env.context.get('mrp_display'):
employee = self.env.user.employee_id
else:
connected_employees = Employee.get_employees_connected()
if not connected_employees:
raise UserError(_("You need to log in to process this work order."))
employee = Employee.get_session_owner()
if employee:
# Stop productivity only for the current employee
self.stop_employee([employee.id])
super().button_pending()
def action_mark_as_done(self):
if self.env.context.get('mrp_display'):
main_employee_connected = self.env['hr.employee'].get_session_owner()
else:
main_employee_connected = self.env.user.employee_id.id
for wo in self:
if not main_employee_connected:
raise UserError(_('You must be logged in to process some of these work orders.'))
if len(wo.allowed_employees) != 0 and main_employee_connected not in [wo.id for wo in wo.allowed_employees]:
raise UserError(_('You are not allow to work on some of these work orders.'))
if wo.working_state == 'blocked':
raise UserError(_('Please unblock the work center to start the work order'))
self.button_finish()
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'productive')], limit=1)
if len(loss_id) < 1:
raise UserError(_("You need to define at least one productivity loss in the category 'Productive'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
self.state = 'done'
self._set_default_time_log(loss_id)
def _set_default_time_log(self, loss_id):
productivity = []
for wo in self:
if not wo.time_ids:
now = fields.Datetime.now()
date_start = datetime.fromtimestamp(now.timestamp() - ((wo.duration_expected * 60) // 1))
date_end = now
connected_employee = wo._get_connected_employee()
productivity.append({
'workorder_id': wo.id,
'workcenter_id': wo.workcenter_id.id,
'description': _('Time Tracking: %(user)s', user=connected_employee.name),
'date_start': date_start,
'date_end': date_end,
'loss_id': loss_id[0].id,
'user_id': self.env.user.id,
'company_id': wo.company_id.id,
'employee_id': connected_employee.id,
})
self.env['mrp.workcenter.productivity'].create(productivity)
def _compute_expected_operation_cost(self, without_employee_cost=False):
expected_machine_cost = super()._compute_expected_operation_cost()
if without_employee_cost:
return expected_machine_cost
expected_labour_cost = (self.duration_expected / 60) * self.workcenter_id.employee_costs_hour * (self.operation_id.employee_ratio or 1)
return expected_machine_cost + expected_labour_cost
def _get_current_theorical_operation_cost(self, without_employee_cost=False):
current_machine_cost = super()._get_current_theorical_operation_cost()
if without_employee_cost:
return current_machine_cost
return current_machine_cost + self._get_current_theorical_employee_cost()
def _get_current_theorical_employee_cost(self):
return (self.get_duration() / 60) * self.workcenter_id.employee_costs_hour * (self.operation_id.employee_ratio or 1)
def _compute_current_operation_cost(self):
current_machine_cost = super()._compute_current_operation_cost()
current_labour_cost = sum(self.time_ids.mapped('total_cost'))
return current_machine_cost + current_labour_cost
def end_all(self):
self.employee_ids = [Command.clear()]
return super().end_all()
def action_mrp_workorder_dependencies(self, action_name):
action = self.env['ir.actions.act_window']._for_xml_id('mrp.action_mrp_workorder_%s' % action_name)
ref = 'mrp_workorder.workcenter_line_gantt_production_dependencies' if action_name == 'production' else 'mrp_workorder.mrp_workorder_view_gantt_dependencies'
if self.env.user.has_group('mrp.group_mrp_workorder_dependencies'):
action['views'] = [(self.env.ref(ref).id, 'gantt')] + [(id, kind) for id, kind in action['views'] if kind != 'gantt']
return action
def _prepare_timeline_vals(self, duration, date_start, date_end=False):
time_data = super()._prepare_timeline_vals(duration=duration, date_start=date_start, date_end=date_end)
employee = self._get_connected_employee()
time_data['employee_id'] = employee.id
time_data['description'] = _('Time Tracking: %(user)s', user=employee.name)
return time_data
def set_qty_producing(self):
self.ensure_one()
self.production_id.set_qty_producing()
# Find first uncompleted step of type register production (if it exists) in the chain
current_check = self.check_ids.filtered(lambda c: not c.previous_check_id)[:1]
while current_check and current_check.next_check_id and (current_check.test_type != 'register_production' or current_check.quality_state != 'none'):
current_check = current_check.next_check_id
if current_check and current_check.test_type == 'register_production' and current_check.quality_state == 'none':
# Check exists already, mark as done.
current_check.action_next()
elif not any(c.test_type == 'register_production' for c in self.check_ids):
# Add a register production step for this WO only (at the front of the list)
first_check = self.check_ids.filtered(lambda c: not c.previous_check_id)
new_check = self.env['quality.check'].create([{
'title': _('Register Production'),
'test_type_id': self.env.ref('mrp_workorder.test_type_register_production').id,
'workorder_id': self.id,
'quality_state': 'pass',
'production_id': self.production_id.id,
'product_id': self.product_id.id,
'lot_ids': self.production_id.lot_producing_ids.ids,
'team_id': self.env['quality.alert.team']._get_quality_team(
self.env['quality.alert.team']._check_company_domain(
self.company_id.id or self.env.context.get('default_company_id', self.env.company.id)
))
}])
if first_check:
new_check._insert_in_chain('before', first_check)
else:
self.current_quality_check_id = new_check
def _get_connected_employee(self):
if self.env.context.get('mrp_display'):
if (self.env.context.get('employee_id')):
connected_employee_id = self.env.context.get('employee_id')
else:
connected_employee_id = self.env['hr.employee'].get_session_owner()
connected_employee = self.env['hr.employee'].browse(connected_employee_id)
else:
if self.employee_assigned_ids:
connected_employee = self.employee_assigned_ids[0]
else:
connected_employee = self.env.user.employee_id
return connected_employee