[REF] mrp: workorder onchange
Create the workorders at the onchange when setting the bom and not a "plan" anymore. task-2241471
This commit is contained in:
committed by
Simon Lejeune
parent
5011ec520c
commit
182f1b8e26
@@ -672,6 +672,10 @@
|
||||
<field name="date_start" eval="(datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</record>
|
||||
|
||||
<function model="mrp.production" name="_create_workorder">
|
||||
<value eval="[ref('mrp.mrp_production_3')]"/>
|
||||
</function>
|
||||
|
||||
<function model="mrp.production" name="action_confirm">
|
||||
<value eval="[ref('mrp.mrp_production_3')]"/>
|
||||
</function>
|
||||
|
||||
@@ -54,8 +54,6 @@ class MrpAbstractWorkorder(models.AbstractModel):
|
||||
workorder line in order to match the new quantity to consume for each
|
||||
component and their reserved quantity.
|
||||
"""
|
||||
if self.qty_producing <= 0:
|
||||
raise UserError(_('You have to produce at least one %s.') % self.product_uom_id.name)
|
||||
line_values = self._update_workorder_lines()
|
||||
for values in line_values['to_create']:
|
||||
self.env[self._workorder_line_ids()._name].new(values)
|
||||
|
||||
@@ -146,7 +146,6 @@ class MrpProduction(models.Model):
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('confirmed', 'Confirmed'),
|
||||
('planned', 'Planned'),
|
||||
('progress', 'In Progress'),
|
||||
('to_close', 'To Close'),
|
||||
('done', 'Done'),
|
||||
@@ -155,7 +154,6 @@ class MrpProduction(models.Model):
|
||||
store=True, tracking=True,
|
||||
help=" * Draft: The MO is not confirmed yet.\n"
|
||||
" * Confirmed: The MO is confirmed, the stock rules and the reordering of the components are trigerred.\n"
|
||||
" * Planned: The WO are planned.\n"
|
||||
" * In Progress: The production has started (on the MO or on the WO).\n"
|
||||
" * To Close: The production is done, the MO has to be closed.\n"
|
||||
" * Done: The MO is closed, the stock moves are posted. \n"
|
||||
@@ -185,8 +183,7 @@ class MrpProduction(models.Model):
|
||||
'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
|
||||
)
|
||||
workorder_ids = fields.One2many(
|
||||
'mrp.workorder', 'production_id', 'Work Orders',
|
||||
copy=False, readonly=True)
|
||||
'mrp.workorder', 'production_id', 'Work Orders', copy=True)
|
||||
workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count')
|
||||
workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count')
|
||||
move_dest_ids = fields.One2many('stock.move', 'created_production_id',
|
||||
@@ -227,6 +224,7 @@ class MrpProduction(models.Model):
|
||||
priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
|
||||
readonly=True, states={'draft': [('readonly', False)]}, default='1')
|
||||
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
|
||||
is_planned = fields.Boolean('Its Operations are Planned', compute="_compute_is_planned")
|
||||
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
|
||||
production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production', readonly=False, related_sudo=False)
|
||||
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order')
|
||||
@@ -296,6 +294,13 @@ class MrpProduction(models.Model):
|
||||
else:
|
||||
production.bom_has_operations = len(production.bom_id.operation_ids) > 0
|
||||
|
||||
def _compute_is_planned(self):
|
||||
for production in self:
|
||||
if production.workorder_ids:
|
||||
production.is_planned = all(wo.date_planned_start and wo.date_planned_finished for wo in production.workorder_ids)
|
||||
else:
|
||||
production.is_planned = False
|
||||
|
||||
@api.depends('move_raw_ids.delay_alert_date')
|
||||
def _compute_delay_alert_date(self):
|
||||
delay_alert_date_data = self.env['stock.move'].read_group([('id', 'in', self.move_raw_ids.ids), ('delay_alert_date', '!=', False)], ['delay_alert_date:max'], 'raw_material_production_id')
|
||||
@@ -403,14 +408,12 @@ class MrpProduction(models.Model):
|
||||
production.workorder_done_count = count_data.get(production.id, 0)
|
||||
|
||||
@api.depends(
|
||||
'move_raw_ids.state', 'move_finished_ids.state', 'workorder_ids',
|
||||
'workorder_ids.date_planned_start', 'workorder_ids.date_planned_finished',
|
||||
'workorder_ids.state', 'move_raw_ids.quantity_done', 'product_qty', 'qty_producing')
|
||||
'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state',
|
||||
'workorder_ids', 'workorder_ids.state', 'product_qty', 'qty_producing')
|
||||
def _compute_state(self):
|
||||
""" Compute the production state. It use the same process than stock
|
||||
picking. It exists 3 extra steps for production:
|
||||
- planned: Workorder has been launched (workorders only)
|
||||
- progress: At least one item is produced.
|
||||
- progress: At least one item is produced or consumed.
|
||||
- to_close: The quantity produced is greater than the quantity to
|
||||
produce and all work orders has been finished.
|
||||
"""
|
||||
@@ -422,22 +425,16 @@ class MrpProduction(models.Model):
|
||||
production.state = 'draft'
|
||||
elif all(move.state == 'cancel' for move in production.move_raw_ids):
|
||||
production.state = 'cancel'
|
||||
elif all(move.state in ['cancel', 'done'] for move in production.move_raw_ids):
|
||||
if (
|
||||
production.bom_id.consumption == 'flexible'
|
||||
and float_compare(production.qty_producing, production.product_qty, precision_rounding=production.product_uom_id.rounding) == -1
|
||||
):
|
||||
production.state = 'progress'
|
||||
else:
|
||||
production.state = 'done'
|
||||
elif all(move.state in ('cancel', 'done') for move in production.move_raw_ids):
|
||||
production.state = 'done'
|
||||
elif production.qty_producing >= production.product_qty:
|
||||
production.state = 'to_close'
|
||||
elif production.workorder_ids and any(wo_state == 'progress' for wo_state in production.workorder_ids.mapped('state')):
|
||||
elif any(wo_state in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')):
|
||||
production.state = 'progress'
|
||||
elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding) and production.qty_producing < production.product_qty:
|
||||
elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding):
|
||||
production.state = 'progress'
|
||||
elif any(not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding) for move in production.move_raw_ids) :
|
||||
production.state = 'progress'
|
||||
elif production.workorder_ids:
|
||||
production.state = 'planned'
|
||||
else:
|
||||
production.state = 'confirmed'
|
||||
|
||||
@@ -522,6 +519,17 @@ class MrpProduction(models.Model):
|
||||
self.bom_id = False
|
||||
self.product_uom_id = self.product_id.uom_id.id
|
||||
|
||||
@api.onchange('product_qty', 'product_uom_id')
|
||||
def _onchange_product_qty(self):
|
||||
product_qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_uom_id)
|
||||
for workorder in self.workorder_ids:
|
||||
if not workorder.product_uom_id:
|
||||
workorder.product_uom_id = self.product_uom_id
|
||||
workorder.qty_producing = self.product_id.tracking == 'serial' and 1.0 or product_qty
|
||||
workorder.duration_expected = workorder._get_duration_expected()
|
||||
if workorder.date_planned_start and workorder.duration_expected:
|
||||
workorder.date_planned_finished = workorder.date_planned_start + relativedelta(minutes=workorder.duration_expected)
|
||||
|
||||
@api.onchange('bom_id')
|
||||
def _onchange_bom_id(self):
|
||||
if not self.product_id and self.bom_id:
|
||||
@@ -533,7 +541,7 @@ class MrpProduction(models.Model):
|
||||
|
||||
@api.onchange('date_planned_start')
|
||||
def _onchange_date_planned_start(self):
|
||||
if not self.bom_id.operation_ids:
|
||||
if not self.workorder_ids:
|
||||
self.date_planned_finished = self.date_planned_start + datetime.timedelta(hours=1)
|
||||
|
||||
@api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id')
|
||||
@@ -595,6 +603,11 @@ class MrpProduction(models.Model):
|
||||
for move_line, res in vals['to_write']:
|
||||
move_line.update(res)
|
||||
|
||||
@api.onchange('bom_id')
|
||||
def _onchange_workorder_ids(self):
|
||||
if self.bom_id:
|
||||
self._create_workorder()
|
||||
|
||||
def write(self, vals):
|
||||
res = super(MrpProduction, self).write(vals)
|
||||
|
||||
@@ -602,7 +615,7 @@ class MrpProduction(models.Model):
|
||||
if 'date_planned_start' in vals:
|
||||
if production.state in ['done', 'cancel']:
|
||||
raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.'))
|
||||
if production.workorder_ids and not self.env.context.get('force_date', False):
|
||||
if production.state not in ('draft', 'confirmed') and production.workorder_ids and not self.env.context.get('force_date', False):
|
||||
raise UserError(_('You cannot move a planned manufacturing order.'))
|
||||
if ('move_raw_ids' in vals or 'move_finished_ids' in vals) and production.state != 'draft':
|
||||
production._autoconfirm_production()
|
||||
@@ -652,6 +665,34 @@ class MrpProduction(models.Model):
|
||||
self.is_locked = not self.is_locked
|
||||
return True
|
||||
|
||||
def _create_workorder(self):
|
||||
for production in self:
|
||||
if not production.bom_id:
|
||||
continue
|
||||
workorders_values = []
|
||||
|
||||
product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
|
||||
exploded_boms, dummy = production.bom_id.explode(production.product_id, product_qty / production.bom_id.product_qty, picking_type=production.bom_id.picking_type_id)
|
||||
|
||||
for bom, bom_data in exploded_boms:
|
||||
# If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
|
||||
if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)):
|
||||
continue
|
||||
for operation in bom.operation_ids:
|
||||
workorders_values += [{
|
||||
'name': operation.name,
|
||||
'production_id': production.id,
|
||||
'workcenter_id': operation.workcenter_id.id,
|
||||
'product_uom_id': production.product_id.uom_id.id,
|
||||
'operation_id': operation.id,
|
||||
'state': 'pending',
|
||||
'qty_producing': production.product_id.tracking == 'serial' and 1.0 or product_qty,
|
||||
'consumption': production.consumption,
|
||||
}]
|
||||
production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values]
|
||||
for workorder in production.workorder_ids:
|
||||
workorder.duration_expected = workorder._get_duration_expected()
|
||||
|
||||
def _get_finished_move_value(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False):
|
||||
group_orders = self.procurement_group_id.mrp_production_ids
|
||||
move_dest_ids = self.move_dest_ids
|
||||
@@ -823,6 +864,8 @@ class MrpProduction(models.Model):
|
||||
if moves_to_confirm:
|
||||
moves_to_confirm._action_confirm()
|
||||
|
||||
self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm()
|
||||
|
||||
def action_view_mrp_production_childs(self):
|
||||
self.ensure_one()
|
||||
mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids
|
||||
@@ -911,17 +954,17 @@ class MrpProduction(models.Model):
|
||||
production._generate_finished_moves()
|
||||
production.move_raw_ids._adjust_procure_method()
|
||||
(production.move_raw_ids | production.move_finished_ids)._action_confirm()
|
||||
production.workorder_ids._action_confirm()
|
||||
return True
|
||||
|
||||
def action_assign(self):
|
||||
for production in self:
|
||||
production.move_raw_ids._action_assign()
|
||||
production.workorder_ids._refresh_wo_lines()
|
||||
return True
|
||||
|
||||
def button_plan(self):
|
||||
""" Create work orders. And probably do stuff, like things. """
|
||||
orders_to_plan = self.filtered(lambda order: order.bom_id.operation_ids and order.state == 'confirmed')
|
||||
orders_to_plan = self.filtered(lambda order: not order.is_planned)
|
||||
for order in orders_to_plan:
|
||||
order.move_raw_ids.filtered(lambda m: m.state == 'draft')._action_confirm()
|
||||
# `propagate_date` enables the automatic rescheduling which could lead to hard to
|
||||
@@ -929,9 +972,6 @@ class MrpProduction(models.Model):
|
||||
# have their leaves booked in the workcenter calendar. We thus disable the
|
||||
# automatic rescheduling in this scenario.
|
||||
order.move_raw_ids.write({'propagate_date': False})
|
||||
quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty
|
||||
boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id)
|
||||
order._generate_workorders(boms)
|
||||
order._plan_workorders()
|
||||
return True
|
||||
|
||||
@@ -963,9 +1003,10 @@ class MrpProduction(models.Model):
|
||||
vals = {}
|
||||
for workcenter in workcenters:
|
||||
# compute theoretical duration
|
||||
time_cycle = workorder.operation_id.time_cycle
|
||||
cycle_number = float_round(qty_to_produce / workcenter.capacity, precision_digits=0, rounding_method='UP')
|
||||
duration_expected = workcenter.time_start + workcenter.time_stop + cycle_number * time_cycle * 100.0 / workcenter.time_efficiency
|
||||
if workorder.workcenter_id == workcenter:
|
||||
duration_expected = workorder.duration_expected
|
||||
else:
|
||||
duration_expected = workorder._get_duration_expected(alternative_workcenter=workcenter)
|
||||
|
||||
from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected)
|
||||
# If the workcenter is unavailable, try planning on the next one
|
||||
@@ -1015,73 +1056,7 @@ class MrpProduction(models.Model):
|
||||
raise UserError(_("Some work orders are already done, you cannot unplan this manufacturing order."))
|
||||
elif any(wo.state == 'progress' for wo in self.workorder_ids):
|
||||
raise UserError(_("Some work orders have already started, you cannot unplan this manufacturing order."))
|
||||
self.workorder_ids.unlink()
|
||||
|
||||
def _generate_workorders(self, exploded_boms):
|
||||
workorders = self.env['mrp.workorder']
|
||||
original_one = False
|
||||
for bom, bom_data in exploded_boms:
|
||||
# If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
|
||||
if bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids):
|
||||
temp_workorders = self._workorders_create(bom, bom_data)
|
||||
workorders += temp_workorders
|
||||
if temp_workorders: # In order to avoid two "ending work orders"
|
||||
if original_one:
|
||||
temp_workorders[-1].next_work_order_id = original_one
|
||||
original_one = temp_workorders[0]
|
||||
return workorders
|
||||
|
||||
def _workorders_create(self, bom, bom_data):
|
||||
"""
|
||||
:param bom: in case of recursive boms: we could create work orders for child
|
||||
BoMs
|
||||
"""
|
||||
workorders = self.env['mrp.workorder']
|
||||
|
||||
# Initial qty producing
|
||||
quantity = max(self.product_qty - sum(self.move_finished_ids.filtered(lambda move: move.product_id == self.product_id).mapped('quantity_done')), 0)
|
||||
quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id)
|
||||
if self.product_id.tracking == 'serial':
|
||||
quantity = 1.0
|
||||
|
||||
for operation in bom.operation_ids:
|
||||
workorder = workorders.create({
|
||||
'name': operation.name,
|
||||
'production_id': self.id,
|
||||
'workcenter_id': operation.workcenter_id.id,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'operation_id': operation.id,
|
||||
'state': len(workorders) == 0 and 'ready' or 'pending',
|
||||
'qty_producing': quantity,
|
||||
'consumption': self.consumption,
|
||||
})
|
||||
if workorders:
|
||||
workorders[-1].next_work_order_id = workorder.id
|
||||
workorders[-1]._start_nextworkorder()
|
||||
workorders += workorder
|
||||
|
||||
# get the raw moves to attach to this operation
|
||||
moves_raw = self.env['stock.move']
|
||||
for move in self.move_raw_ids:
|
||||
if move.operation_id == operation:
|
||||
moves_raw |= move
|
||||
moves_finished = self.move_finished_ids.filtered(lambda move: move.operation_id == operation)
|
||||
|
||||
# - Raw moves from a BoM where a routing was set but no operation was precised should
|
||||
# be consumed at the last workorder of the linked routing.
|
||||
# - Raw moves from a BoM where no rounting was set should be consumed at the last
|
||||
# workorder of the main routing.
|
||||
if len(workorders) == len(bom.operation_ids):
|
||||
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.operation_id and move.bom_line_id.bom_id.operation_ids and move.bom_line_id.bom_id == bom)
|
||||
moves_raw |= self.move_raw_ids.filtered(lambda move: not move.workorder_id and not move.bom_line_id.bom_id.operation_ids)
|
||||
|
||||
moves_finished |= self.move_finished_ids.filtered(lambda move: move.product_id != self.product_id and not move.operation_id)
|
||||
|
||||
moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id})
|
||||
(moves_finished | moves_raw).write({'workorder_id': workorder.id})
|
||||
|
||||
workorder._generate_wo_lines()
|
||||
return workorders
|
||||
self.workorder_ids.leave_id.unlink()
|
||||
|
||||
def _get_consumption_issues(self):
|
||||
"""Compare the quantity consumed of the components, the expected quantity
|
||||
@@ -1286,7 +1261,7 @@ class MrpProduction(models.Model):
|
||||
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
|
||||
|
||||
for wo in backorder_mo.workorder_ids:
|
||||
wo.duration_expected = wo._get_duration_expected(wo.workcenter_id)
|
||||
wo.duration_expected = wo._get_duration_expected()
|
||||
|
||||
backorders.action_confirm()
|
||||
# Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves
|
||||
@@ -1340,7 +1315,7 @@ class MrpProduction(models.Model):
|
||||
production.write({'date_finished': fields.Datetime.now(), 'product_qty': production.qty_produced})
|
||||
|
||||
for workorder in self.workorder_ids.filtered(lambda w: w.state not in ('done', 'cancel')):
|
||||
workorder.duration_expected = workorder._get_duration_expected(workorder.workcenter_id)
|
||||
workorder.duration_expected = workorder._get_duration_expected()
|
||||
|
||||
if not backorders:
|
||||
return True
|
||||
|
||||
@@ -17,11 +17,12 @@ class MrpRoutingWorkcenter(models.Model):
|
||||
help="Gives the sequence order when displaying a list of routing Work Centers.")
|
||||
bom_id = fields.Many2one(
|
||||
'mrp.bom', 'Bill of Material',
|
||||
index=True, ondelete='cascade', required=True,
|
||||
index=True, ondelete='cascade',
|
||||
help="The Bill of Material this operation is linked to")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
readonly=True, related='bom_id.company_id', store=True)
|
||||
readonly=True, related='bom_id.company_id', store=True,
|
||||
default=lambda self: self.env.company)
|
||||
worksheet_type = fields.Selection([
|
||||
('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')],
|
||||
string="Work Sheet", default="pdf",
|
||||
|
||||
@@ -31,7 +31,7 @@ class MrpWorkorder(models.Model):
|
||||
required=True, index=True, readonly=True)
|
||||
workcenter_id = fields.Many2one(
|
||||
'mrp.workcenter', 'Work Center', required=True,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)], 'progress': [('readonly', True)]},
|
||||
group_expand='_read_group_workcenter_id', check_company=True)
|
||||
working_state = fields.Selection(
|
||||
string='Workcenter Status', related='workcenter_id.working_state', readonly=False,
|
||||
@@ -59,34 +59,32 @@ class MrpWorkorder(models.Model):
|
||||
('progress', 'In Progress'),
|
||||
('done', 'Finished'),
|
||||
('cancel', 'Cancelled')], string='Status',
|
||||
default='pending')
|
||||
default='pending', copy=False, readonly=True)
|
||||
leave_id = fields.Many2one(
|
||||
'resource.calendar.leaves',
|
||||
help='Slot into workcenter calendar once planned',
|
||||
check_company=True)
|
||||
check_company=True, copy=False)
|
||||
date_planned_start = fields.Datetime(
|
||||
'Scheduled Date Start',
|
||||
'Scheduled Start Date',
|
||||
compute='_compute_dates_planned',
|
||||
inverse='_set_dates_planned',
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
store=True,
|
||||
tracking=True)
|
||||
store=True, tracking=True, copy=False)
|
||||
date_planned_finished = fields.Datetime(
|
||||
'Scheduled Date Finished',
|
||||
'Scheduled End Date',
|
||||
compute='_compute_dates_planned',
|
||||
inverse='_set_dates_planned',
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
store=True,
|
||||
tracking=True)
|
||||
store=True, tracking=True, copy=False)
|
||||
date_start = fields.Datetime(
|
||||
'Effective Start Date',
|
||||
'Start Date', copy=False,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
date_finished = fields.Datetime(
|
||||
'Effective End Date',
|
||||
'End Date', copy=False,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||
|
||||
duration_expected = fields.Float(
|
||||
'Expected Duration', digits=(16, 2),
|
||||
'Expected Duration', digits=(16, 2), default=60.0,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
help="Expected duration (in minutes)")
|
||||
duration = fields.Float(
|
||||
@@ -101,8 +99,7 @@ class MrpWorkorder(models.Model):
|
||||
progress = fields.Float('Progress Done (%)', digits=(16, 2), compute='_compute_progress')
|
||||
|
||||
operation_id = fields.Many2one(
|
||||
'mrp.routing.workcenter', 'Operation',
|
||||
check_company=True)
|
||||
'mrp.routing.workcenter', 'Operation', check_company=True)
|
||||
# Should be used differently as BoM can change in the meantime
|
||||
worksheet = fields.Binary(
|
||||
'Worksheet', related='operation_id.worksheet', readonly=True)
|
||||
@@ -124,7 +121,7 @@ class MrpWorkorder(models.Model):
|
||||
'stock.production.lot', 'Lot/Serial Number', domain="[('id', 'in', allowed_lots_domain)]",
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, check_company=True)
|
||||
time_ids = fields.One2many(
|
||||
'mrp.workcenter.productivity', 'workorder_id')
|
||||
'mrp.workcenter.productivity', 'workorder_id', copy=False)
|
||||
is_user_working = fields.Boolean(
|
||||
'Is the Current User Working', compute='_compute_working_users',
|
||||
help="Technical field indicating whether the current user is working. ")
|
||||
@@ -144,6 +141,7 @@ class MrpWorkorder(models.Model):
|
||||
json_popover = fields.Char('Popover Data JSON', compute='_compute_json_popover')
|
||||
show_json_popover = fields.Boolean('Show Popover?', compute='_compute_json_popover')
|
||||
|
||||
@api.depends('production_state', 'date_planned_start', 'date_planned_finished')
|
||||
def _compute_json_popover(self):
|
||||
previous_wo_data = self.env['mrp.workorder'].read_group(
|
||||
[('next_work_order_id', 'in', self.ids)],
|
||||
@@ -154,9 +152,14 @@ class MrpWorkorder(models.Model):
|
||||
'date_planned_start': x['date_planned_start'],
|
||||
'date_planned_finished': x['date_planned_finished']})
|
||||
for x in previous_wo_data])
|
||||
conflicted_dict = self._get_conflicted_workorder_ids()
|
||||
if self.ids:
|
||||
conflicted_dict = self._get_conflicted_workorder_ids()
|
||||
for wo in self:
|
||||
infos = []
|
||||
if not wo.date_planned_start or not wo.date_planned_finished or not wo.ids:
|
||||
wo.show_json_popover = False
|
||||
wo.json_popover = False
|
||||
continue
|
||||
if wo.state in ['pending', 'ready']:
|
||||
previous_wo = previous_wo_dict.get(wo.id)
|
||||
prev_start = previous_wo and previous_wo['date_planned_start'] or False
|
||||
@@ -222,24 +225,6 @@ class MrpWorkorder(models.Model):
|
||||
else:
|
||||
workorder.is_finished_lines_editable = False
|
||||
|
||||
@api.onchange('finished_lot_id')
|
||||
def _onchange_finished_lot_id(self):
|
||||
"""When the user changes the lot being currently produced, suggest
|
||||
a quantity to produce consistent with the previous workorders. """
|
||||
previous_wo = self.env['mrp.workorder'].search([
|
||||
('next_work_order_id', '=', self.id)
|
||||
])
|
||||
if previous_wo:
|
||||
line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
|
||||
if line:
|
||||
self.qty_producing = line.qty_done
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
diff = self.date_planned_finished - self.date_planned_start
|
||||
self.duration_expected = diff.total_seconds() / 60
|
||||
|
||||
@api.depends('production_id.workorder_ids.finished_workorder_line_ids',
|
||||
'production_id.workorder_ids.finished_workorder_line_ids.qty_done',
|
||||
'production_id.workorder_ids.finished_workorder_line_ids.lot_id')
|
||||
@@ -251,6 +236,8 @@ class MrpWorkorder(models.Model):
|
||||
productions = self.mapped('production_id')
|
||||
treated = self.browse()
|
||||
for production in productions:
|
||||
if production.state == 'draft':
|
||||
continue
|
||||
if production.product_id.tracking == 'none':
|
||||
continue
|
||||
|
||||
@@ -297,12 +284,17 @@ class MrpWorkorder(models.Model):
|
||||
# Removes references to workorder to avoid Validation Error
|
||||
(self.mapped('move_raw_ids') | self.mapped('move_finished_ids')).write({'workorder_id': False})
|
||||
self.mapped('leave_id').unlink()
|
||||
return super(MrpWorkorder, self).unlink()
|
||||
mo_dirty = self.production_id.filtered(lambda mo: mo.state in ("confirmed", "progress", "to_close"))
|
||||
res = super().unlink()
|
||||
# We need to go through `_action_confirm` for all workorders of the current productions to
|
||||
# make sure the links between them are correct (`next_work_order_id` could be obsolete now).
|
||||
mo_dirty.workorder_ids._action_confirm()
|
||||
return res
|
||||
|
||||
@api.depends('production_id.product_qty', 'qty_produced')
|
||||
@api.depends('production_id.product_qty', 'qty_produced', 'production_id.product_uom_id')
|
||||
def _compute_is_produced(self):
|
||||
self.is_produced = False
|
||||
for order in self.filtered(lambda p: p.production_id):
|
||||
for order in self.filtered(lambda p: p.production_id and p.production_id.product_uom_id):
|
||||
rounding = order.production_id.product_uom_id.rounding
|
||||
order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0
|
||||
|
||||
@@ -347,11 +339,39 @@ class MrpWorkorder(models.Model):
|
||||
for workorder in self:
|
||||
workorder.scrap_count = count_data.get(workorder.id, 0)
|
||||
|
||||
@api.onchange('finished_lot_id')
|
||||
def _onchange_finished_lot_id(self):
|
||||
"""When the user changes the lot being currently produced, suggest
|
||||
a quantity to produce consistent with the previous workorders. """
|
||||
previous_wo = self.env['mrp.workorder'].search([
|
||||
('next_work_order_id', '=', self.id)
|
||||
])
|
||||
if previous_wo:
|
||||
line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
|
||||
if line:
|
||||
self.qty_producing = line.qty_done
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
diff = self.date_planned_finished - self.date_planned_start
|
||||
self.duration_expected = diff.total_seconds() / 60
|
||||
|
||||
@api.onchange('operation_id')
|
||||
def _onchange_operation_id(self):
|
||||
if self.operation_id:
|
||||
self.name = self.operation_id.name
|
||||
self.workcenter_id = self.operation_id.workcenter_id.id
|
||||
|
||||
@api.onchange('date_planned_start', 'duration_expected')
|
||||
def _onchange_date_planned_start(self):
|
||||
if self.date_planned_start and self.duration_expected:
|
||||
self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected)
|
||||
|
||||
@api.onchange('operation_id', 'workcenter_id', 'qty_production')
|
||||
def _onchange_expected_duration(self):
|
||||
self.duration_expected = self._get_duration_expected()
|
||||
|
||||
def write(self, values):
|
||||
if 'production_id' in values:
|
||||
raise UserError(_('You cannot link this work order to another manufacturing order.'))
|
||||
@@ -361,7 +381,7 @@ class MrpWorkorder(models.Model):
|
||||
if workorder.state in ('progress', 'done', 'cancel'):
|
||||
raise UserError(_('You cannot change the workcenter of a work order that is in progress or done.'))
|
||||
workorder.leave_id.resource_id = self.env['mrp.workcenter'].browse(values['workcenter_id']).resource_id
|
||||
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
|
||||
if any(k not in ['time_ids', 'duration_expected', 'next_work_order_id'] for k in values.keys()) and any(workorder.state == 'done' for workorder in self):
|
||||
raise UserError(_('You can not change the finished work order.'))
|
||||
if 'date_planned_start' in values or 'date_planned_finished' in values:
|
||||
for workorder in self:
|
||||
@@ -381,6 +401,67 @@ class MrpWorkorder(models.Model):
|
||||
})
|
||||
return super(MrpWorkorder, self).write(values)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
res = super().create(values)
|
||||
# Auto-confirm manually added workorders.
|
||||
# We need to go through `_action_confirm` for all workorders of the current productions to
|
||||
# make sure the links between them are correct.
|
||||
to_confirm = res.filtered(lambda wo: wo.production_id.state in ("confirmed", "progress", "to_close"))
|
||||
to_confirm = to_confirm.production_id.workorder_ids
|
||||
to_confirm._action_confirm()
|
||||
return res
|
||||
|
||||
def _action_confirm(self):
|
||||
workorders_by_production = defaultdict(lambda: self.env['mrp.workorder'])
|
||||
for workorder in self:
|
||||
workorders_by_production[workorder.production_id] |= workorder
|
||||
|
||||
for production, workorders in workorders_by_production.items():
|
||||
workorders_by_bom = defaultdict(lambda: self.env['mrp.workorder'])
|
||||
bom = self.env['mrp.bom']
|
||||
moves = production.move_raw_ids | production.move_finished_ids
|
||||
|
||||
for workorder in self:
|
||||
if workorder.operation_id.bom_id:
|
||||
bom = workorder.operation_id.bom_id
|
||||
if not bom:
|
||||
bom = workorder.production_id.bom_id
|
||||
previous_workorder = workorders_by_bom[bom][-1:]
|
||||
previous_workorder.next_work_order_id = workorder.id
|
||||
workorders_by_bom[bom] |= workorder
|
||||
|
||||
moves.filtered(lambda m: m.operation_id == workorder.operation_id).write({
|
||||
'workorder_id': workorder.id
|
||||
})
|
||||
|
||||
exploded_boms, dummy = production.bom_id.explode(production.product_id, 1, picking_type=production.bom_id.picking_type_id)
|
||||
exploded_boms = {b[0]: b[1] for b in exploded_boms}
|
||||
for move in moves:
|
||||
if move.workorder_id:
|
||||
continue
|
||||
bom = move.bom_line_id.bom_id
|
||||
while bom and bom not in workorders_by_bom:
|
||||
bom_data = exploded_boms.get(bom, {})
|
||||
bom = bom_data.get('parent_line') and bom_data['parent_line'].bom_id or False
|
||||
if bom in workorders_by_bom:
|
||||
move.write({
|
||||
'workorder_id': workorders_by_bom[bom][-1:].id
|
||||
})
|
||||
else:
|
||||
move.write({
|
||||
'workorder_id': workorders_by_bom[production.bom_id][-1:].id
|
||||
})
|
||||
|
||||
for workorder in workorders:
|
||||
workorder._apply_update_workorder_lines()
|
||||
|
||||
for workorders in workorders_by_bom.values():
|
||||
if workorders[0].state == 'pending':
|
||||
workorders[0].state = 'ready'
|
||||
for workorder in workorders:
|
||||
workorder._start_nextworkorder()
|
||||
|
||||
def _generate_wo_lines(self):
|
||||
""" Generate workorder line """
|
||||
self.ensure_one()
|
||||
@@ -570,6 +651,8 @@ class MrpWorkorder(models.Model):
|
||||
(self.operation_id.batch == 'yes' and
|
||||
float_compare(self.operation_id.batch_size, self.qty_produced, precision_rounding=rounding) <= 0)):
|
||||
self.next_work_order_id.state = 'ready'
|
||||
if self.state == 'done' and self.next_work_order_id.state == 'pending':
|
||||
self.next_work_order_id.state = 'ready'
|
||||
|
||||
@api.model
|
||||
def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None):
|
||||
@@ -617,7 +700,7 @@ class MrpWorkorder(models.Model):
|
||||
|
||||
# Need a loss in case of the real time exceeding the expected
|
||||
timeline = self.env['mrp.workcenter.productivity']
|
||||
if self.duration < self.duration_expected:
|
||||
if not self.duration_expected or self.duration < self.duration_expected:
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
|
||||
if not len(loss_id):
|
||||
raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
|
||||
@@ -632,7 +715,7 @@ class MrpWorkorder(models.Model):
|
||||
timeline.create({
|
||||
'workorder_id': self.id,
|
||||
'workcenter_id': self.workcenter_id.id,
|
||||
'description': _('Time Tracking: ')+self.env.user.name,
|
||||
'description': _('Time Tracking: ') + self.env.user.name,
|
||||
'loss_id': loss_id[0].id,
|
||||
'date_start': datetime.now(),
|
||||
'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id
|
||||
@@ -640,26 +723,45 @@ class MrpWorkorder(models.Model):
|
||||
})
|
||||
if self.state == 'progress':
|
||||
return True
|
||||
start_date = datetime.now()
|
||||
vals = {
|
||||
'state': 'progress',
|
||||
'date_start': start_date,
|
||||
}
|
||||
if not self.leave_id:
|
||||
leave = self.env['resource.calendar.leaves'].create({
|
||||
'name': self.production_id.name + ' - ' + self.name,
|
||||
'calendar_id': self.workcenter_id.resource_calendar_id.id,
|
||||
'date_from': start_date,
|
||||
'date_to': start_date + relativedelta(minutes=self.duration_expected),
|
||||
'resource_id': self.workcenter_id.resource_id.id,
|
||||
'time_type': 'other'
|
||||
})
|
||||
vals['leave_id'] = leave.id
|
||||
return self.write(vals)
|
||||
else:
|
||||
start_date = datetime.now()
|
||||
vals = {
|
||||
'state': 'progress',
|
||||
'date_start': start_date,
|
||||
'date_planned_start': start_date,
|
||||
}
|
||||
vals['date_planned_start'] = start_date
|
||||
if self.date_planned_finished < start_date:
|
||||
vals['date_planned_finished'] = start_date
|
||||
return self.write(vals)
|
||||
|
||||
def button_finish(self):
|
||||
self.ensure_one()
|
||||
self.end_all()
|
||||
end_date = datetime.now()
|
||||
return self.write({
|
||||
'state': 'done',
|
||||
'date_finished': end_date,
|
||||
'date_planned_finished': end_date
|
||||
})
|
||||
for workorder in self:
|
||||
if workorder.state in ('done', 'cancel'):
|
||||
continue
|
||||
workorder.end_all()
|
||||
vals = {
|
||||
'state': 'done',
|
||||
'date_finished': end_date,
|
||||
'date_planned_finished': end_date
|
||||
}
|
||||
if not workorder.date_start:
|
||||
vals['date_start'] = end_date
|
||||
if not workorder.date_planned_start or end_date < workorder.date_planned_start:
|
||||
vals['date_planned_start'] = end_date
|
||||
workorder.write(vals)
|
||||
return True
|
||||
|
||||
def end_previous(self, doall=False):
|
||||
"""
|
||||
@@ -751,11 +853,32 @@ class MrpWorkorder(models.Model):
|
||||
action['domain'] = [('workorder_id', '=', self.id)]
|
||||
return action
|
||||
|
||||
def action_open_wizard(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('mrp.mrp_workorder_mrp_production_form').read()[0]
|
||||
action['res_id'] = self.id
|
||||
return action
|
||||
|
||||
@api.depends('qty_production', 'qty_produced')
|
||||
def _compute_qty_remaining(self):
|
||||
for wo in self:
|
||||
wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
|
||||
|
||||
def _get_duration_expected(self, alternative_workcenter=False):
|
||||
self.ensure_one()
|
||||
if not self.workcenter_id:
|
||||
return False
|
||||
qty_production = self.production_id.product_uom_id._compute_quantity(self.qty_production, self.production_id.product_id.uom_id)
|
||||
cycle_number = float_round(qty_production / self.workcenter_id.capacity, precision_digits=0, rounding_method='UP')
|
||||
if alternative_workcenter:
|
||||
# TODO : find a better alternative : the settings of workcenter can change
|
||||
duration_expected_working = (self.duration_expected - self.workcenter_id.time_start - self.workcenter_id.time_stop) * self.workcenter_id.time_efficiency / (100.0 * cycle_number)
|
||||
if duration_expected_working < 0:
|
||||
duration_expected_working = 0
|
||||
return alternative_workcenter.time_start + alternative_workcenter.time_stop + cycle_number * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency
|
||||
time_cycle = self.operation_id and self.operation_id.time_cycle or 60.0
|
||||
return self.workcenter_id.time_start + self.workcenter_id.time_stop + cycle_number * time_cycle * 100.0 / self.workcenter_id.time_efficiency
|
||||
|
||||
def _get_conflicted_workorder_ids(self):
|
||||
"""Get conlicted workorder(s) with self.
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class StockRule(models.Model):
|
||||
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
|
||||
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values)
|
||||
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
|
||||
productions._create_workorder()
|
||||
productions.filtered(lambda p: p.move_raw_ids).action_confirm()
|
||||
|
||||
for production in productions:
|
||||
|
||||
@@ -24,6 +24,9 @@ var MrpWorkorderPopover = PopoverWidget.extend({
|
||||
|
||||
_render: function () {
|
||||
this._super.apply(this, arguments);
|
||||
if (! this.$popover) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
this.$popover.find('.action_replan_button').click(function (e) {
|
||||
self._onReplanClick(e);
|
||||
|
||||
@@ -347,19 +347,14 @@ class TestMrpWorkorderBackorder(SavepointCase):
|
||||
self.assertEqual(workorder1.raw_workorder_line_ids.product_id, self.compfinished1)
|
||||
self.assertEqual(workorder1.raw_workorder_line_ids.qty_to_consume, 2)
|
||||
|
||||
#mrp_production_workorder_form_view_inherit
|
||||
workorder1.button_start()
|
||||
with Form(workorder1) as w:
|
||||
w.qty_producing = 1
|
||||
workorder1.record_production()
|
||||
workorder1.button_finish()
|
||||
|
||||
self.assertEqual(workorder2.raw_workorder_line_ids.product_id, self.compfinished2)
|
||||
self.assertEqual(workorder2.raw_workorder_line_ids.qty_to_consume, 2)
|
||||
|
||||
workorder2.button_start()
|
||||
with Form(workorder2) as w:
|
||||
w.qty_producing = 1
|
||||
workorder2.record_production()
|
||||
workorder2.button_finish()
|
||||
|
||||
action = mo.with_context(debug=True).button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
|
||||
@@ -82,13 +82,32 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
done and the WO must be cancelled.
|
||||
"""
|
||||
# Create MO
|
||||
mo_data = self.generate_mo(consumption='strict')
|
||||
manufacturing_order = mo_data[0]
|
||||
bom = mo_data[1]
|
||||
bom.write({
|
||||
product_to_build = self.env['product.product'].create({
|
||||
'name': 'Young Tom',
|
||||
'type': 'product',
|
||||
})
|
||||
product_to_use_1 = self.env['product.product'].create({
|
||||
'name': 'Botox',
|
||||
'type': 'product',
|
||||
})
|
||||
product_to_use_2 = self.env['product.product'].create({
|
||||
'name': 'Old Tom',
|
||||
'type': 'product',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_to_build.id,
|
||||
'product_tmpl_id': product_to_build.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'strict',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_to_use_2.id, 'product_qty': 4}),
|
||||
(0, 0, {'product_id': product_to_use_1.id, 'product_qty': 1})
|
||||
],
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
]
|
||||
})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.product_id = product_to_build
|
||||
@@ -96,9 +115,6 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
mo_form.product_qty = 5.0
|
||||
manufacturing_order = mo_form.save()
|
||||
manufacturing_order.action_confirm()
|
||||
mo_form = Form(manufacturing_order)
|
||||
mo_form.qty_producing = 2
|
||||
manufacturing_order = mo_form.save()
|
||||
|
||||
manufacturing_order.button_plan()
|
||||
workorder = manufacturing_order.workorder_ids
|
||||
|
||||
@@ -995,6 +995,53 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
|
||||
self.assertEqual(production_table.workorder_ids[0].qty_producing, 2, "Quantity to produce not updated")
|
||||
|
||||
def test_expected_duration_workorder(self):
|
||||
""" Test that the expected_duration is editable and change when other
|
||||
workcenter is set or product quantity to produce is increased
|
||||
Also this expected duration need to be recalculate correctly during the planning
|
||||
if it used a alternative workcenter
|
||||
"""
|
||||
planned_date = datetime(2023, 5, 15, 9, 0)
|
||||
self.mrp_workcenter_3.write({'capacity': 1, 'time_start': 0, 'time_stop': 2, 'time_efficiency': 50})
|
||||
self.workcenter_1.write({'capacity': 2, 'time_start': 2, 'time_stop': 1, 'time_efficiency': 80, 'alternative_workcenter_ids': [(6, 0, [self.mrp_workcenter_3.id])]})
|
||||
self.planning_bom.operation_ids[0].write({'time_cycle_manual': 10})
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = self.planning_bom
|
||||
mo_form.date_planned_start = planned_date
|
||||
mo = mo_form.save()
|
||||
|
||||
# TEST Default/onchange duration_expected
|
||||
self.assertEqual(len(mo.workorder_ids), 1)
|
||||
self.assertEqual(mo.workorder_ids.workcenter_id, self.workcenter_1, "wrong workcenter")
|
||||
# time_cycle = 10, product_qty = 4.0, capacity = 2, time_start = 2, time_stop = 1, time_efficiency = 80%
|
||||
# => 2 + 1 + (4 / 2 * 10) * (100 / 80) = 28.0
|
||||
self.assertEqual(mo.workorder_ids.duration_expected, 28.0, "wrong duration_expected")
|
||||
mo_form = Form(mo)
|
||||
with mo_form.workorder_ids.edit(0) as wo_form:
|
||||
wo_form.workcenter_id = self.mrp_workcenter_3
|
||||
mo = mo_form.save()
|
||||
# time_cycle = 10, product_qty = 4.0, capacity = 1, time_start = 0, time_stop = 2, time_efficiency = 50%
|
||||
# => 0 + 2 + (4 / 1 * 10) * (100 / 50) = 82.0
|
||||
self.assertEqual(mo.workorder_ids.duration_expected, 82.0, "wrong duration_expected")
|
||||
|
||||
# TEST Manual change of duration_expected
|
||||
with mo_form.workorder_ids.edit(0) as wo_form:
|
||||
wo_form.workcenter_id = self.workcenter_1
|
||||
self.assertEqual(wo_form.duration_expected, 28.0, "wrong duration_expected")
|
||||
wo_form.duration_expected = 40.0
|
||||
mo = mo_form.save()
|
||||
|
||||
self.assertEqual(mo.workorder_ids.duration_expected, 40.0, "wrong duration_expected")
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.button_plan()
|
||||
self.assertEqual(mo.workorder_ids.duration_expected, 40.0, "duration_expected doesn't reset")
|
||||
|
||||
self.assertAlmostEqual(mo.date_planned_start, mo.workorder_ids.date_planned_start, delta=timedelta(seconds=10))
|
||||
self.assertAlmostEqual(mo.date_planned_finished, mo.workorder_ids.date_planned_finished, delta=timedelta(seconds=10))
|
||||
self.assertAlmostEqual(mo.date_planned_start, planned_date, delta=timedelta(seconds=10))
|
||||
self.assertAlmostEqual(mo.date_planned_finished, planned_date + timedelta(minutes=40), delta=timedelta(seconds=10))
|
||||
|
||||
def test_planning_0(self):
|
||||
""" Test alternative conditions
|
||||
1. alternative relation is directionnal
|
||||
@@ -1253,7 +1300,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
self.assertEqual(bool(leave.exists()), True)
|
||||
# Unplans the MO and checks the workorder and its leave no more exist.
|
||||
mo.button_unplan()
|
||||
self.assertEqual(bool(mo.workorder_ids.exists()), False)
|
||||
self.assertEqual(bool(mo.workorder_ids.exists()), True)
|
||||
self.assertEqual(bool(leave.exists()), False)
|
||||
# Plans (again) the MO and checks the date is still the same.
|
||||
mo.button_plan()
|
||||
|
||||
@@ -96,6 +96,15 @@
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Operations"
|
||||
name="operations"
|
||||
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
|
||||
groups="mrp.group_mrp_routings">
|
||||
<field name="operation_ids"
|
||||
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
|
||||
groups="mrp.group_mrp_routings"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
</page>
|
||||
<page string="By-products"
|
||||
name="by_products"
|
||||
attrs="{'invisible': [('type','!=','normal')]}"
|
||||
@@ -122,15 +131,6 @@
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Operations"
|
||||
name="operations"
|
||||
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
|
||||
groups="mrp.group_mrp_routings">
|
||||
<field name="operation_ids"
|
||||
attrs="{'invisible': [('type','not in',('normal','phantom'))]}"
|
||||
groups="mrp.group_mrp_routings"
|
||||
context="{'default_company_id': company_id}"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
<field name="confirm_cancel" invisible="1"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('qty_producing', '=', 0)]}" string="Backorder" type="object" class="oe_highlight"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
|
||||
<button name="action_confirm" attrs="{'invisible': [('state', '!=', 'draft')]}" string="Confirm" type="object" class="oe_highlight"/>
|
||||
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
|
||||
<button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Confirm" type="object" class="oe_highlight"/>
|
||||
<button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
|
||||
<button name="button_plan" attrs="{'invisible': ['|', '|', ('state', 'not in', ('confirmed', 'progress')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" type="object" string="Plan" class="oe_highlight"/>
|
||||
<button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start', '=', False), ('date_planned_finished', '=', False)]}"/>
|
||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/>
|
||||
@@ -86,14 +86,9 @@
|
||||
<field name="unreserve_visible" invisible="1"/>
|
||||
<field name="reserve_visible" invisible="1"/>
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="is_planned" invisible="1"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/>
|
||||
<button name="%(action_mrp_workorder_production_specific)d" type="action" attrs="{'invisible': [('workorder_count', '=', 0)]}" class="oe_stat_button" icon="fa-play-circle-o">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="workorder_done_count" widget="statinfo" nolabel="1"/> / <field name="workorder_count" widget="statinfo" nolabel="1"/></span>
|
||||
<span class="o_stat_text">Work Orders</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
|
||||
<button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" groups="base.group_user" attrs="{'invisible': [('delivery_count', '=', 0)]}">
|
||||
<field name="delivery_count" widget="statinfo" string="Transfers"/>
|
||||
@@ -321,6 +316,44 @@
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Operations" name="operations" groups="mrp.group_mrp_routings">
|
||||
<field name="workorder_ids" attrs="{'readonly': [('state', 'in', ['cancel', 'done'])]}" context="{'default_production_id': id, 'default_qty_producing': product_qty, 'default_product_uom_id': product_uom_id, 'default_consumption': consumption, 'default_company_id': company_id}">
|
||||
<tree editable="bottom">
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_produced" invisible="1"/>
|
||||
<field name="is_user_working" invisible="1"/>
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="product_uom_id" invisible="1" readonly="0"/>
|
||||
<field name="qty_producing" invisible="1"/>
|
||||
<field name="time_ids" invisible="1"/>
|
||||
<field name="working_state" invisible="1"/>
|
||||
<field name="operation_id" invisible="1" domain="['|', ('bom_id', '=', parent.bom_id), ('bom_id', '=', False)]" context="{'default_workcenter_id': workcenter_id, 'default_company_id': company_id}"/>
|
||||
<field name="name" string="Operation"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="date_planned_start" optional="show"/>
|
||||
<field name="date_planned_finished" optional="hide"/>
|
||||
<field name="date_start" optional="hide" readonly="1"/>
|
||||
<field name="date_finished" optional="hide" readonly="1"/>
|
||||
<field name="duration_expected" widget="float_time" sum="Total excepted duration"/>
|
||||
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."
|
||||
attrs="{'invisible': ['|', ('parent.state','=', 'draft'), ('time_ids', '=', [])]}"/>
|
||||
<button name="action_open_wizard" type="object" icon="fa-external-link" width="0" class="oe_edit_only"
|
||||
context="{'default_workcenter_id': workcenter_id}"/>
|
||||
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/>
|
||||
<button name="button_start" type="object" string="Start" width="0" class="btn-success"
|
||||
attrs="{'invisible': ['|', '|', '|', ('parent.state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
|
||||
<button name="button_pending" type="object" string="Pause" width="0" class="btn-warning"
|
||||
attrs="{'invisible': ['|', '|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_finish" type="object" string="Done" width="0" class="btn-success"
|
||||
attrs="{'invisible': ['|', '|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="%(mrp.act_mrp_block_workcenter_wo)d" type="action" string="Block" context="{'default_workcenter_id': workcenter_id}" width="0" class="btn-danger"
|
||||
attrs="{'invisible': ['|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked')]}"/>
|
||||
<button name="button_unblock" type="object" string="Unblock" context="{'default_workcenter_id': workcenter_id}" width="0" class="btn-danger"
|
||||
attrs="{'invisible': ['|', ('parent.state', 'in', ('draft', 'done')), ('working_state', '!=', 'blocked')]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Miscellaneous" name="miscellaneous" groups="stock.group_stock_multi_locations">
|
||||
<group>
|
||||
<group>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration"/>
|
||||
<field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration" width="1.5"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -431,14 +431,6 @@
|
||||
parent="menu_mrp_configuration"
|
||||
sequence="90"/>
|
||||
|
||||
<menuitem id="menu_mrp_dashboard"
|
||||
name="Overview"
|
||||
action="mrp_workcenter_kanban_action"
|
||||
groups="group_mrp_routings"
|
||||
parent="menu_mrp_root"
|
||||
sequence="5"/>
|
||||
|
||||
|
||||
<record id="oee_loss_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.workcenter.productivity.loss.form</field>
|
||||
<field name="model">mrp.workcenter.productivity.loss</field>
|
||||
|
||||
@@ -92,15 +92,6 @@
|
||||
<field name="allowed_lots_domain" invisible="1"/>
|
||||
<field name="is_finished_lines_editable" invisible="1"/>
|
||||
<header>
|
||||
<button name="button_finish" type="object" string="Finish Order" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('is_produced', '=', False)]}" class="btn-info"/>
|
||||
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'pending')]}"/>
|
||||
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'ready')]}" class="btn-success"/>
|
||||
<button name="record_production" type="object" string="Done" class="btn-success" attrs="{'invisible': ['|', '|', '|', ('is_produced', '=', True), ('working_state', '=', 'blocked'), ('state', '!=', 'progress'), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_pending" type="object" string="Pause" class="btn-warning" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/>
|
||||
<button name="%(act_mrp_block_workcenter_wo)d" type="action" context="{'default_workcenter_id': workcenter_id}" string="Block" class="btn-danger" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/>
|
||||
<button name="button_unblock" type="object" string="Unblock" class="btn-danger" attrs="{'invisible': [('working_state', '!=', 'blocked')]}"/>
|
||||
<button name="button_start" type="object" string="Continue Production" attrs="{'invisible': ['|', '|', '|', ('production_state', '=', 'done'), ('working_state', '=', 'blocked'), ('is_user_working', '=', True), ('state', '!=', 'progress')]}"/>
|
||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending,ready,progress,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
@@ -112,50 +103,24 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="product_id" string="To Produce"/>
|
||||
<label for="qty_produced" string="Quantity Produced"/>
|
||||
<div class="o_row">
|
||||
<field name="qty_produced"/> /
|
||||
<field name="qty_production"/>
|
||||
<field name="product_uom_id"/>
|
||||
<field name="production_availability" nolabel="1" widget="bullet_state" options="{'classes': {'assigned': 'success', 'waiting': 'danger'}}" attrs="{'invisible': [('state', '=', 'done')]}"/>
|
||||
</div>
|
||||
<field name="is_produced" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="production_id" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<field name="workcenter_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="production_id" invisible="1"/>
|
||||
<field name="finished_lot_id" invisible="1"/>
|
||||
<field name="qty_producing" invisible="1"/>
|
||||
<group attrs="{'invisible': [('date_planned_start', '=', False)]}">
|
||||
<label for="date_planned_start" string="Planned Date"/>
|
||||
<div class="oe_inline">
|
||||
<field name="date_planned_start" class="mr8 oe_inline" required="True"/>
|
||||
<strong class="mr8 oe_inline">to</strong>
|
||||
<field name="date_planned_finished" class="oe_inline" required="True"/>
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
<field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
|
||||
</div>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Time Tracking" name="time_tracking" groups="mrp.group_mrp_manager">
|
||||
<group>
|
||||
<label for="date_planned_start" string="Planned Date"/>
|
||||
<div class="oe_inline">
|
||||
<field name="date_planned_start" class="mr8 oe_inline" required="True"/>
|
||||
<strong class="mr8 oe_inline">to</strong>
|
||||
<field name="date_planned_finished" class="oe_inline" required="True"/>
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
<field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/>
|
||||
</div>
|
||||
<label for="date_start" string="Effective Date" attrs="{'invisible': [('date_start', '=', False)]}"/>
|
||||
<div class="o_row" attrs="{'invisible': [('date_start', '=', False)]}">
|
||||
<field name="date_start" readonly="1"/>
|
||||
<div attrs="{'invisible': [('date_finished', '=', False)]}">
|
||||
<strong class="mr8">to</strong>
|
||||
<field name="date_finished" readonly="1"/>
|
||||
</div>
|
||||
</div>
|
||||
<label for="duration_expected"/>
|
||||
<div>
|
||||
<field name="duration_expected" widget="float_time" class="oe_inline"/>
|
||||
minutes
|
||||
</div>
|
||||
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="time_ids" nolabel="1" context="{'default_workcenter_id': workcenter_id, 'default_workorder_id': id}">
|
||||
<tree>
|
||||
@@ -165,7 +130,7 @@
|
||||
<field name="user_id"/>
|
||||
<field name="workcenter_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="loss_id" string="Productivity"/>
|
||||
<field name="loss_id" string="Productivity" optional="hide"/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
@@ -186,44 +151,6 @@
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Current Production" name="current_production">
|
||||
<group>
|
||||
<group>
|
||||
<field name="qty_producing" string="Quantity in Production" attrs="{'readonly': ['|', ('product_tracking', '=', 'serial'), ('state', 'in', ('done', 'cancel'))]}"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="finished_lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', '=', 'none')]}" groups="stock.group_production_lot"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="consumption" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<h4 attrs="{'invisible': ['&', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])]}">Components</h4>
|
||||
<field name="raw_workorder_line_ids" attrs="{'invisible': ['&', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])], 'readonly': [('state', 'in', ('cancel', 'done'))]}" options="{'create': [('consumption', '=', 'flexible')]}">
|
||||
<tree editable="bottom" create="1" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id, 'active_mo_id': parent.production_id}"/>
|
||||
<field name="qty_to_consume" attrs="{'readonly': [('parent.consumption', '=', 'strict')]}" force_save="1"/>
|
||||
<field name="qty_reserved" readonly="1"/>
|
||||
<field name="qty_done" attrs="{'column_invisible': [('parent.state', 'not in', ('progress', 'done'))]}"/>
|
||||
<field name="product_uom_id" invisible="1"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<h4>Finished Products</h4>
|
||||
<field name="finished_workorder_line_ids" attrs="{'readonly': [('is_finished_lines_editable', '=', False)]}">
|
||||
<tree editable="bottom" create="1" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="product_tracking" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id}"/>
|
||||
<field name="qty_to_consume" readonly="1" string="To Produce" force_save="1"/>
|
||||
<field name="qty_done" string="Produced"/>
|
||||
<field name="product_uom_id" invisible="1"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Work Instruction" name="workorder_page_work_instruction" attrs="{'invisible': [('worksheet', '=', False), ('worksheet_google_slide', '=', False), ('operation_note', '=', False)]}">
|
||||
<field name="worksheet_type" invisible="1"/>
|
||||
<field name="worksheet" widget="pdf_viewer" attrs="{'invisible': [('worksheet_type', '!=', 'pdf')]}"/>
|
||||
@@ -485,6 +412,15 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mrp_workorder_mrp_production_form">
|
||||
<field name="name">Work Orders</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.workorder</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="mrp_production_workorder_form_view_inherit"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mrp_workorder_todo">
|
||||
<field name="name">Work Orders</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
|
||||
Reference in New Issue
Block a user