summaryrefslogtreecommitdiff
path: root/addons/stock
diff options
context:
space:
mode:
authorNicolas Martinelli <nim@odoo.com>2020-02-14 14:47:37 +0000
committerNicolas Martinelli <nim@odoo.com>2020-02-18 09:38:36 +0000
commit1f68f33c97c986cc8d16009ed74222adbcf41f45 (patch)
tree32bd6ef7f8f00bcdfe3a16a94eb739d63c955c2b /addons/stock
parentb1cee5cc668b929f4179d80e597195d2836de816 (diff)
[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
Diffstat (limited to 'addons/stock')
-rw-r--r--addons/stock/models/stock_move.py38
-rw-r--r--addons/stock/tests/test_move2.py169
2 files changed, 205 insertions, 2 deletions
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 740854283dc..dd93a25a484 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -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'
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index f037dce934a..6b7b822e0af 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -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.