[IMP] mrp: MO component qty changes reflected in pickings

When changing quantities of raw moves after MO confirm,
procurement is run to fulfill updated need.

Since procurement creates new moves, some fixes were also done to allow
moves to be merged with older ones, including:
1) `date_planned_start` of merged MO included microsecond which
blocked merging new moves created by procurement with orig moves.
Removed microseconds.

2) `_set_date_deadline` did not allow a simple date update to moves with
orig or dest moves. Added context to hard set all moves in self to specific
deadline.

3) `_update_candidate_moves_list` when merging moves, did not return candidates
from pickings that were merged to same MO. Usually just mattered when decreasing
quantity. Added explicit check for sibling pickings in MO

closes odoo/odoo#93712

Task: 2859547
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
This commit is contained in:
Ahmed Khalaf (ahkh)
2023-01-13 16:26:57 +01:00
committed by Ahmed Khalaf
parent 475f8aa2c6
commit 1f4fb64a19
6 changed files with 178 additions and 29 deletions
+6 -17
View File
@@ -32,7 +32,7 @@ class MrpProduction(models.Model):
def _get_default_date_planned_start(self):
if self.env.context.get('default_date_deadline'):
return fields.Datetime.to_datetime(self.env.context.get('default_date_deadline'))
return datetime.datetime.now()
return fields.Datetime.now()
@api.model
def _get_default_is_locked(self):
@@ -1074,27 +1074,13 @@ class MrpProduction(models.Model):
def _update_raw_moves(self, factor):
self.ensure_one()
update_info = []
moves_to_assign = self.env['stock.move']
procurements = []
for move in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')):
old_qty = move.product_uom_qty
new_qty = float_round(old_qty * factor, precision_rounding=move.product_uom.rounding, rounding_method='UP')
if new_qty > 0:
# procurement and assigning is now run in write
move.write({'product_uom_qty': new_qty})
if move._should_bypass_reservation() \
or move.picking_type_id.reservation_method == 'at_confirm' \
or (move.reservation_date and move.reservation_date <= fields.Date.today()):
moves_to_assign |= move
if move.procure_method == 'make_to_order':
procurement_qty = new_qty - old_qty
values = move._prepare_procurement_values()
procurements.append(self.env['procurement.group'].Procurement(
move.product_id, procurement_qty, move.product_uom,
move.location_id, move.name, move.origin, move.company_id, values))
update_info.append((move, old_qty, new_qty))
moves_to_assign._action_assign()
if procurements:
self.env['procurement.group'].run(procurements)
return update_info
@api.ondelete(at_uninstall=False)
@@ -1614,7 +1600,7 @@ class MrpProduction(models.Model):
move_to_backorder_moves[move] = self.env['stock.move']
unit_factor = move.product_uom_qty / initial_qty_by_production[production]
initial_move_vals = move.copy_data(move._get_backorder_move_vals())[0]
move.with_context(do_not_unreserve=True).product_uom_qty = production.product_qty * unit_factor
move.with_context(do_not_unreserve=True, no_procurement=True).product_uom_qty = production.product_qty * unit_factor
for backorder in production_to_backorders[production]:
move_vals = dict(
@@ -2055,6 +2041,7 @@ class MrpProduction(models.Model):
for move in production.move_raw_ids:
for field, vals in origs[move.bom_line_id.id].items():
move[field] = vals
for move in production.move_finished_ids:
move.move_dest_ids = [Command.set(dests[move.byproduct_id.id])]
@@ -2069,6 +2056,8 @@ class MrpProduction(models.Model):
production.state = 'confirmed'
self.with_context(skip_activity=True)._action_cancel()
# set the new deadline of origin moves (stock to pre prod)
production.move_raw_ids.move_orig_ids.with_context(date_deadline_propagate_ids=set(production.move_raw_ids.ids)).write({'date_deadline': production.date_planned_start})
for p in self:
p._message_log(body=_('This production has been merge in %s', production.display_name))
+1 -1
View File
@@ -619,7 +619,7 @@ class MrpWorkorder(models.Model):
})
if self.state == 'progress':
return True
start_date = datetime.now()
start_date = fields.Datetime.now()
vals = {
'state': 'progress',
'date_start': start_date,
+35 -4
View File
@@ -304,8 +304,35 @@ class StockMove(models.Model):
# so possibly unlink lines
move_line_vals = vals.pop('move_line_ids')
super().write({'move_line_ids': move_line_vals})
if 'product_uom_qty' in vals and self.raw_material_production_id.state == 'confirmed' and not self.env.context.get('no_procurement', False):
# when updating consumed qty need to update related pickings
# context no_procurement means we don't want the qty update to modify stock i.e create new pickings
# ex. when spliting MO to backorders we don't want to move qty from pre prod to stock in 2/3 step config
self._run_procurement(vals['product_uom_qty'], self.product_uom_qty)
return super().write(vals)
def _run_procurement(self, new_qty, old_qty):
procurements = []
to_assign = self.env['stock.move']
self._adjust_procure_method()
for move in self:
if new_qty > 0:
if move._should_bypass_reservation() \
or move.picking_type_id.reservation_method == 'at_confirm' \
or (move.reservation_date and move.reservation_date <= fields.Date.today()):
to_assign |= move
if move.procure_method == 'make_to_order':
procurement_qty = new_qty - old_qty
values = move._prepare_procurement_values()
procurements.append(self.env['procurement.group'].Procurement(
move.product_id, procurement_qty, move.product_uom,
move.location_id, move.name, move.origin, move.company_id, values))
to_assign._action_assign()
if procurements:
self.env['procurement.group'].run(procurements)
def _action_assign(self, force_qty=False):
res = super(StockMove, self)._action_assign(force_qty=force_qty)
for move in self.filtered(lambda x: x.production_id or x.raw_material_production_id):
@@ -531,12 +558,16 @@ class StockMove(models.Model):
else:
self.quantity_done = new_qty
def _update_candidate_moves_list(self, candidate_moves_list):
super()._update_candidate_moves_list(candidate_moves_list)
def _update_candidate_moves_list(self, candidate_moves_set):
super()._update_candidate_moves_list(candidate_moves_set)
for production in self.mapped('raw_material_production_id'):
candidate_moves_list.append(production.move_raw_ids)
candidate_moves_set.add(production.move_raw_ids)
for production in self.mapped('production_id'):
candidate_moves_list.append(production.move_finished_ids)
candidate_moves_set.add(production.move_finished_ids)
# this will include sibling pickings as a result of merging MOs
for picking in self.move_dest_ids.raw_material_production_id.picking_ids:
candidate_moves_set.add(picking.move_ids)
def _multi_line_quantity_done_set(self, quantity_done):
if self.raw_material_production_id:
+129
View File
@@ -751,3 +751,132 @@ class TestProcurement(TestMrpCommon):
{'product_qty': 1, 'bom_id': bom01.id, 'picking_type_id': manu_operation01.id, 'location_dest_id': stock_location01.id},
{'product_qty': 2, 'bom_id': bom02.id, 'picking_type_id': manu_operation02.id, 'location_dest_id': stock_location02.id},
])
def test_update_mo_component_qty(self):
""" After Confirming MO, updating component qty should run procurement
to update orig move qty
"""
warehouse = self.env['stock.warehouse'].search([], limit=1)
# 2 steps Manufacture
warehouse.write({'manufacture_steps': 'pbm'})
mo, *_ = self.generate_mo(qty_final=2, qty_base_1=1, qty_base_2=2)
self.assertEqual(mo.state, 'confirmed', 'MO should be confirmed at this point')
self.assertEqual(mo.product_qty, 2, 'MO qty to produce should be 2')
self.assertEqual(mo.move_raw_ids.mapped('product_uom_qty'), [4, 2], 'Comp2 qty should be 4 and comp1 should be 2')
self.assertEqual(mo.picking_ids.move_ids.mapped('product_uom_qty'), [4, 2], 'Comp moves should have same qty as MO')
# decrease comp2 qty, should reflect in picking
mo.move_raw_ids[0].product_uom_qty = 2
self.assertEqual(mo.picking_ids.move_ids[0].product_uom_qty, 2, 'Comp2 move should have same qty as MO')
# add a third component, should reflect in picking
comp3 = self.env['product.product'].create({
'name': 'Comp3',
'type': 'product'
})
mo.write({
'move_raw_ids': [(0, 0, {
'product_id': comp3.id,
'product_uom_qty': 3
})]
})
self.assertEqual(len(mo.picking_ids.move_ids), 3, 'Picking should have 3 moves')
self.assertEqual(mo.picking_ids.move_ids[2].product_uom_qty, 3, 'Comp3 move should have same qty as MO')
# change its qty
mo.move_raw_ids[2].product_uom_qty = 4
self.assertEqual(mo.picking_ids.move_ids[2].product_uom_qty, 4, 'Comp3 move should have same qty as MO')
# increase qty to produce
wiz = self.env['change.production.qty'].create({
'mo_id': mo.id,
'product_qty': 4
})
wiz.change_prod_qty()
self.assertEqual(mo.product_qty, 4, 'MO qty to produce should be 4')
# each move qty should be doubled
self.assertEqual(mo.picking_ids.move_ids.mapped('product_uom_qty'), [4, 4, 8], 'Comps move should have same qty as MO')
def test_update_merged_mo_component_qty(self):
""" After Confirming two MOs merge then and change their component qtys,
Procurements should run and any new moves should be merged with old ones
"""
warehouse = self.env['stock.warehouse'].search([], limit=1)
# 2 steps Manufacture
warehouse.write({'manufacture_steps': 'pbm'})
super_product = self.env['product.product'].create({
'name': 'Super Product',
'type': 'product',
})
comp1 = self.env['product.product'].create({
'name': 'Comp1',
'type': 'product',
})
comp2 = self.env['product.product'].create({
'name': 'Comp2',
'type': 'product',
})
bom = self.env['mrp.bom'].create({
'product_id': super_product.id,
'product_tmpl_id': super_product.product_tmpl_id.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 1.0,
'type': 'normal',
'consumption': 'flexible',
'bom_line_ids': [
(0, 0, {'product_id': comp1.id, 'product_qty': 1}),
(0, 0, {'product_id': comp2.id, 'product_qty': 2})
]
})
# MO 1
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = super_product
mo_form.bom_id = bom
mo_form.product_qty = 1
mo_1 = mo_form.save()
mo_1.action_confirm()
# MO 2
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = super_product
mo_form.bom_id = bom
mo_form.product_qty = 1
mo_2 = mo_form.save()
mo_2.action_confirm()
res_mo_id = (mo_1 | mo_2).action_merge()['res_id']
mo = self.env['mrp.production'].browse(res_mo_id)
self.assertEqual(mo.product_qty, 2, 'Qty to produce should be 2')
self.assertEqual(mo.move_raw_ids.mapped('product_uom_qty'), [2, 4], 'Comp1 qty should be 2 and comp2 should be 4')
self.assertEqual(mo.picking_ids[0].move_ids.mapped('product_uom_qty'), [1, 2], 'Comp moves should have same qty as old MO')
# increase Comp1 qty by 1 in MO
mo.move_raw_ids[0].product_uom_qty = 3
# any required qty is added to first picking by procurement
self.assertEqual(mo.picking_ids[0].move_ids[0].product_uom_qty, 2, 'Comp1 qty increase should reflect in picking')
# add new comp3
comp3 = self.env['product.product'].create({
'name': 'Comp3',
'type': 'product'
})
mo.write({
'move_raw_ids': [(0, 0, {
'product_id': comp3.id,
'product_uom_qty': 2,
})]
})
self.assertEqual(len(mo.picking_ids[0].move_ids), 3, 'Picking should have 3 moves')
self.assertEqual(mo.picking_ids[0].move_ids[2].product_uom_qty, 2, 'Comp3 move should have same qty as MO')
# increase qty to produce
wiz = self.env['change.production.qty'].create({
'mo_id': mo.id,
'product_qty': 4
})
wiz.change_prod_qty()
self.assertEqual(mo.product_qty, 4, 'MO qty to produce should be 4')
# extra quantities are all added to first picking moves
# comp1 (2 + 3 extra) = 5
# comp2 (2 + 4 extra) = 6
# comp3 (2 + 2 extra) = 4
self.assertEqual(mo.picking_ids[0].move_ids.mapped('product_uom_qty'), [5, 6, 4], 'Comp qty do not match expected')
@@ -31,7 +31,7 @@ class MrpProductionBackorder(models.TransientModel):
wizard.show_backorder_lines = len(wizard.mrp_production_backorder_line_ids) > 1
def action_close_mo(self):
return self.mrp_production_ids.with_context(skip_backorder=True).button_mark_done()
return self.mrp_production_ids.with_context(skip_backorder=True, no_procurement=True).button_mark_done()
def action_backorder(self):
ctx = dict(self.env.context)
+6 -6
View File
@@ -900,9 +900,9 @@ Please change the quantity done or the rounding precision of your unit of measur
"""Cleanup hook used when merging moves"""
self.write({'propagate_cancel': False})
def _update_candidate_moves_list(self, candidate_moves_list):
def _update_candidate_moves_list(self, candidate_moves_set):
for picking in self.mapped('picking_id'):
candidate_moves_list.append(picking.move_ids)
candidate_moves_set.add(picking.move_ids)
def _merge_moves(self, merge_into=False):
""" This method will, for each move in `self`, go up in their linked picking and try to
@@ -912,11 +912,11 @@ Please change the quantity done or the rounding precision of your unit of measur
"""
distinct_fields = self._prepare_merge_moves_distinct_fields()
candidate_moves_list = []
candidate_moves_set = set()
if not merge_into:
self._update_candidate_moves_list(candidate_moves_list)
self._update_candidate_moves_list(candidate_moves_set)
else:
candidate_moves_list.append(merge_into | self)
candidate_moves_set.add(merge_into | self)
# Move removed after merge
moves_to_unlink = self.env['stock.move']
@@ -933,7 +933,7 @@ Please change the quantity done or the rounding precision of your unit of measur
excluded_fields = self._prepare_merge_negative_moves_excluded_distinct_fields()
neg_key = itemgetter(*[field for field in distinct_fields if field not in excluded_fields])
for candidate_moves in candidate_moves_list:
for candidate_moves in candidate_moves_set:
# First step find move to merge.
candidate_moves = candidate_moves.filtered(lambda m: m.state not in ('done', 'cancel', 'draft')) - neg_qty_moves
for __, g in groupby(candidate_moves, key=itemgetter(*distinct_fields)):