[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:
committed by
Ahmed Khalaf
parent
475f8aa2c6
commit
1f4fb64a19
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)):
|
||||
|
||||
Reference in New Issue
Block a user