Files
odoo_source/addons/mrp/wizard/change_production_qty.py
T
JF Aubert dbd09f16a2 [IMP] mrp,mrp_workorder: workorder scheduling revamp
Currently MRP only allows sequential scheduling of work orders.

This commit allows to specify the dependencies for work orders to be
scheduled in parallel or in a complex diagram.

By enabling the feature in the settings and at the bom level,
you can specify dependencies between operations:
on the operation form, select the operations on which the
current one depends.

The dependencies will be reflected in work orders during planning.

The workorders Gantt view has been adapted for you to visualize
and manipulate the dependencies.

Please note:
- work orders without predecessors will be started first
- they can all be done in parallel
- when all work orders of this step are finished, their successors
  will start
- and so on, step by step

Also, with the feature activated at the bom level and no dependency,
the parallel scheme will be applied (all operations at first step).

Finally, without activation at the bom level, the dependencies will
be created in order to satisfy the current sequential model.

Task: 2691328
Part-of: odoo/odoo#87131
2022-06-03 16:40:08 +02:00

116 lines
6.2 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero, float_round
class ChangeProductionQty(models.TransientModel):
_name = 'change.production.qty'
_description = 'Change Production Qty'
mo_id = fields.Many2one('mrp.production', 'Manufacturing Order',
required=True, ondelete='cascade')
product_qty = fields.Float(
'Quantity To Produce',
digits='Product Unit of Measure', required=True)
@api.model
def default_get(self, fields):
res = super(ChangeProductionQty, self).default_get(fields)
if 'mo_id' in fields and not res.get('mo_id') and self._context.get('active_model') == 'mrp.production' and self._context.get('active_id'):
res['mo_id'] = self._context['active_id']
if 'product_qty' in fields and not res.get('product_qty') and res.get('mo_id'):
res['product_qty'] = self.env['mrp.production'].browse(res['mo_id']).product_qty
return res
@api.model
def _update_finished_moves(self, production, new_qty, old_qty):
""" Update finished product and its byproducts. This method only update
the finished moves not done or cancel and just increase or decrease
their quantity according the unit_ratio. It does not use the BoM, BoM
modification during production would not be taken into consideration.
"""
modification = {}
push_moves = self.env['stock.move']
for move in production.move_finished_ids:
if move.state in ('done', 'cancel'):
continue
qty = (new_qty - old_qty) * move.unit_factor
modification[move] = (move.product_uom_qty + qty, move.product_uom_qty)
if self._need_quantity_propagation(move, qty):
push_moves |= move.copy({'product_uom_qty': qty})
else:
move.write({'product_uom_qty': move.product_uom_qty + qty})
if push_moves:
push_moves._action_confirm()._action_assign()
return modification
@api.model
def _need_quantity_propagation(self, move, qty):
return move.move_dest_ids and not float_is_zero(qty, precision_rounding=move.product_uom.rounding)
def change_prod_qty(self):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
for wizard in self:
production = wizard.mo_id
produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done'))
if wizard.product_qty < produced:
format_qty = '%.{precision}f'.format(precision=precision)
raise UserError(_(
"You have already processed %(quantity)s. Please input a quantity higher than %(minimum)s ",
quantity=format_qty % produced,
minimum=format_qty % produced
))
old_production_qty = production.product_qty
new_production_qty = wizard.product_qty
done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id)
qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id)
factor = (new_production_qty - qty_produced) / (old_production_qty - qty_produced)
update_info = production._update_raw_moves(factor)
documents = {}
for move, old_qty, new_qty in update_info:
iterate_key = production._get_document_iterate_key(move)
if iterate_key:
document = self.env['stock.picking']._log_activity_get_documents({move: (new_qty, old_qty)}, iterate_key, 'UP')
for key, value in document.items():
if documents.get(key):
documents[key] += [value]
else:
documents[key] = [value]
production._log_manufacture_exception(documents)
self._update_finished_moves(production, new_production_qty - qty_produced, old_production_qty - qty_produced)
production.write({'product_qty': new_production_qty})
for wo in production.workorder_ids:
operation = wo.operation_id
wo.duration_expected = wo._get_duration_expected(ratio=new_production_qty / old_production_qty)
quantity = wo.qty_production - wo.qty_produced
if production.product_id.tracking == 'serial':
quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0
else:
quantity = quantity if (quantity > 0 and not float_is_zero(quantity, precision_digits=precision)) else 0
wo._update_qty_producing(quantity)
if wo.qty_produced < wo.qty_production and wo.state == 'done':
wo.state = 'progress'
if wo.qty_produced == wo.qty_production and wo.state == 'progress':
wo.state = 'done'
# assign moves; last operation receive all unassigned moves
# TODO: following could be put in a function as it is similar as code in _workorders_create
# TODO: only needed when creating new moves
moves_raw = production.move_raw_ids.filtered(lambda move: move.operation_id == operation and move.state not in ('done', 'cancel'))
if wo == production.workorder_ids[-1]:
moves_raw |= production.move_raw_ids.filtered(lambda move: not move.operation_id)
moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products?
moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id})
(moves_finished + moves_raw).write({'workorder_id': wo.id})
# run scheduler for moves forecasted to not have enough in stock
self.mo_id.filtered(lambda mo: mo.state in ['confirmed', 'progress']).move_raw_ids._trigger_scheduler()
return {}