[FIX] stock: MTSO in manufacturing
- Install MRP and Purchase - Configure the rule 'Stock → YourCompany: Production (MTO)' with: Supply Method: Take From Stock, if unavailable, Trigger Another Rule - Create a product AB with route 'Manufacture' - Create products A & B with routes 'Buy' and 'Replenish on Order (MTO)', set a supplier - Create a BOM for AB with: Product A: 2.0 Units Product B: 3.0 Units - Create a MO for 1.0 Unit of AB, 'Mark as Todo' No RFQ is created for A & B while there is no stock available and a supplier is set. This is because the 'MTSO' logic is located in `_run_pull`, which is never called in this use case since no procurement is created. We need to apply the same logic in `_adjust_procure_method`, which is called at MO confirmation. opw-2189694 opw-2194739 X-original-commit: 9ae3b3e8d2694f5560913682824fd10e60de14a4
This commit is contained in:
@@ -1548,6 +1548,15 @@ class StockMove(models.Model):
|
||||
""" This method will try to apply the procure method MTO on some moves if
|
||||
a compatible MTO route is found. Else the procure method will be set to MTS
|
||||
"""
|
||||
# Prepare the MTSO variables. They are needed since MTSO moves are handled separately.
|
||||
# We need 2 dicts:
|
||||
# - needed quantity per location per product
|
||||
# - forecasted quantity per location per product
|
||||
mtso_products_by_locations = defaultdict(list)
|
||||
mtso_needed_qties_by_loc = defaultdict(dict)
|
||||
mtso_free_qties_by_loc = {}
|
||||
mtso_moves = self.env['stock.move']
|
||||
|
||||
for move in self:
|
||||
product_id = move.product_id
|
||||
domain = [
|
||||
@@ -1556,7 +1565,32 @@ class StockMove(models.Model):
|
||||
('action', '!=', 'push')
|
||||
]
|
||||
rules = self.env['procurement.group']._search_rule(False, product_id, move.warehouse_id, domain)
|
||||
if rules and (rules.procure_method == 'make_to_order'):
|
||||
move.procure_method = rules.procure_method
|
||||
if rules:
|
||||
if rules.procure_method in ['make_to_order', 'make_to_stock']:
|
||||
move.procure_method = rules.procure_method
|
||||
else:
|
||||
# Get the needed quantity for the `mts_else_mto` moves.
|
||||
mtso_needed_qties_by_loc[rules.location_src_id].setdefault(product_id.id, 0)
|
||||
mtso_needed_qties_by_loc[rules.location_src_id][product_id.id] += move.product_qty
|
||||
|
||||
# This allow us to get the forecasted quantity in batch later on
|
||||
mtso_products_by_locations[rules.location_src_id].append(product_id.id)
|
||||
mtso_moves |= move
|
||||
else:
|
||||
move.procure_method = 'make_to_stock'
|
||||
|
||||
# Get the forecasted quantity for the `mts_else_mto` moves.
|
||||
for location, product_ids in mtso_products_by_locations.items():
|
||||
products = self.env['product.product'].browse(product_ids).with_context(location=location.id)
|
||||
mtso_free_qties_by_loc[location] = {product.id: product.free_qty for product in products}
|
||||
|
||||
# Now that we have the needed and forecasted quantity per location and per product, we can
|
||||
# choose whether the mtso_moves need to be MTO or MTS.
|
||||
for move in mtso_moves:
|
||||
needed_qty = move.product_qty
|
||||
forecasted_qty = mtso_free_qties_by_loc[move.location_id][move.product_id.id]
|
||||
if float_compare(needed_qty, forecasted_qty, precision_rounding=product_id.uom_id.rounding) <= 0:
|
||||
move.procure_method = 'make_to_stock'
|
||||
mtso_free_qties_by_loc[move.location_id][move.product_id.id] -= needed_qty
|
||||
else:
|
||||
move.procure_method = 'make_to_order'
|
||||
|
||||
@@ -2390,6 +2390,175 @@ class TestRoutes(TestStockCommon):
|
||||
self.assertEqual(len(picking.move_lines), 1)
|
||||
self.assertEqual(picking.move_lines.product_uom_qty, 2)
|
||||
|
||||
def test_mtso_mto_adjust_01(self):
|
||||
""" Run '_adjust_procure_method' for products A & B:
|
||||
- Product A has 5.0 available
|
||||
- Product B has 3.0 available
|
||||
Stock moves (SM) are created for 4.0 units
|
||||
After '_adjust_procure_method':
|
||||
- SM for A is 'make_to_stock'
|
||||
- SM for B is 'make_to_order'
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
final_location = self.partner.property_stock_customer
|
||||
product_A = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
})
|
||||
product_B = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# We alter one rule and we set it to 'mts_else_mto'
|
||||
rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
|
||||
rule.procure_method = 'mts_else_mto'
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(product_B, warehouse.lot_stock_id, 3.0)
|
||||
|
||||
move_tmpl = {
|
||||
'name': 'Product',
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 4.0,
|
||||
'location_id': warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.partner.property_stock_customer.id,
|
||||
'warehouse_id': warehouse.id,
|
||||
}
|
||||
move_A_vals = dict(move_tmpl)
|
||||
move_A_vals.update({
|
||||
'product_id': product_A.id,
|
||||
})
|
||||
move_A = self.env['stock.move'].create(move_A_vals)
|
||||
move_B_vals = dict(move_tmpl)
|
||||
move_B_vals.update({
|
||||
'product_id': product_B.id,
|
||||
})
|
||||
move_B = self.env['stock.move'].create(move_B_vals)
|
||||
moves = move_A + move_B
|
||||
|
||||
self.assertEqual(move_A.procure_method, 'make_to_stock', 'Move A should be "make_to_stock"')
|
||||
self.assertEqual(move_B.procure_method, 'make_to_stock', 'Move A should be "make_to_order"')
|
||||
moves._adjust_procure_method()
|
||||
self.assertEqual(move_A.procure_method, 'make_to_stock', 'Move A should be "make_to_stock"')
|
||||
self.assertEqual(move_B.procure_method, 'make_to_order', 'Move A should be "make_to_order"')
|
||||
|
||||
def test_mtso_mto_adjust_02(self):
|
||||
""" Run '_adjust_procure_method' for products A & B:
|
||||
- Product A has 5.0 available
|
||||
- Product B has 3.0 available
|
||||
Stock moves (SM) are created for 2.0 + 2.0 units
|
||||
After '_adjust_procure_method':
|
||||
- SM for A is 'make_to_stock'
|
||||
- SM for B is 'make_to_stock' and 'make_to_order'
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
final_location = self.partner.property_stock_customer
|
||||
product_A = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
})
|
||||
product_B = self.env['product.product'].create({
|
||||
'name': 'Product B',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# We alter one rule and we set it to 'mts_else_mto'
|
||||
rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
|
||||
rule.procure_method = 'mts_else_mto'
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(product_B, warehouse.lot_stock_id, 3.0)
|
||||
|
||||
move_tmpl = {
|
||||
'name': 'Product',
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
'location_id': warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.partner.property_stock_customer.id,
|
||||
'warehouse_id': warehouse.id,
|
||||
}
|
||||
move_A1_vals = dict(move_tmpl)
|
||||
move_A1_vals.update({
|
||||
'product_id': product_A.id,
|
||||
})
|
||||
move_A1 = self.env['stock.move'].create(move_A1_vals)
|
||||
move_A2_vals = dict(move_tmpl)
|
||||
move_A2_vals.update({
|
||||
'product_id': product_A.id,
|
||||
})
|
||||
move_A2 = self.env['stock.move'].create(move_A2_vals)
|
||||
move_B1_vals = dict(move_tmpl)
|
||||
move_B1_vals.update({
|
||||
'product_id': product_B.id,
|
||||
})
|
||||
move_B1 = self.env['stock.move'].create(move_B1_vals)
|
||||
move_B2_vals = dict(move_tmpl)
|
||||
move_B2_vals.update({
|
||||
'product_id': product_B.id,
|
||||
})
|
||||
move_B2 = self.env['stock.move'].create(move_B2_vals)
|
||||
moves = move_A1 + move_A2 + move_B1 + move_B2
|
||||
|
||||
self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
|
||||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
|
||||
self.assertEqual(move_B1.procure_method, 'make_to_stock', 'Move B1 should be "make_to_stock"')
|
||||
self.assertEqual(move_B2.procure_method, 'make_to_stock', 'Move B2 should be "make_to_stock"')
|
||||
moves._adjust_procure_method()
|
||||
self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
|
||||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
|
||||
self.assertEqual(move_B1.procure_method, 'make_to_stock', 'Move B1 should be "make_to_stock"')
|
||||
self.assertEqual(move_B2.procure_method, 'make_to_order', 'Move B2 should be "make_to_order"')
|
||||
|
||||
def test_mtso_mto_adjust_03(self):
|
||||
""" Run '_adjust_procure_method' for products A with 4.0 available
|
||||
2 Stock moves (SM) are created:
|
||||
- SM1 for 5.0 Units
|
||||
- SM2 for 3.0 Units
|
||||
SM1 is confirmed, so 'virtual_available' is -1.0.
|
||||
SM1 should become 'make_to_order'
|
||||
SM2 should remain 'make_to_stock'
|
||||
"""
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
|
||||
final_location = self.partner.property_stock_customer
|
||||
product_A = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
})
|
||||
|
||||
# We alter one rule and we set it to 'mts_else_mto'
|
||||
rule = self.env['procurement.group']._get_rule(product_A, final_location, {'warehouse_id': warehouse})
|
||||
rule.procure_method = 'mts_else_mto'
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(product_A, warehouse.lot_stock_id, 4.0)
|
||||
|
||||
move_tmpl = {
|
||||
'name': 'Product',
|
||||
'product_id': product_A.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'location_id': warehouse.lot_stock_id.id,
|
||||
'location_dest_id': self.partner.property_stock_customer.id,
|
||||
'warehouse_id': warehouse.id,
|
||||
}
|
||||
move_A1_vals = dict(move_tmpl)
|
||||
move_A1_vals.update({
|
||||
'product_uom_qty': 5.0,
|
||||
})
|
||||
move_A1 = self.env['stock.move'].create(move_A1_vals)
|
||||
move_A2_vals = dict(move_tmpl)
|
||||
move_A2_vals.update({
|
||||
'product_uom_qty': 3.0,
|
||||
})
|
||||
move_A2 = self.env['stock.move'].create(move_A2_vals)
|
||||
moves = move_A1 + move_A2
|
||||
|
||||
self.assertEqual(move_A1.procure_method, 'make_to_stock', 'Move A1 should be "make_to_stock"')
|
||||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A2 should be "make_to_stock"')
|
||||
move_A1._action_confirm()
|
||||
moves._adjust_procure_method()
|
||||
self.assertEqual(move_A1.procure_method, 'make_to_order', 'Move A should be "make_to_stock"')
|
||||
self.assertEqual(move_A2.procure_method, 'make_to_stock', 'Move A should be "make_to_order"')
|
||||
|
||||
def test_delay_alert_1(self):
|
||||
""" On a pick pack ship scenario, enable the delay alert flag on the pack rule. Edit the
|
||||
schedule date on the pick, a delay alert should be created for the ship.
|
||||
|
||||
Reference in New Issue
Block a user