diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py
index 96374d4830a..84c7e44fe90 100644
--- a/addons/stock/models/stock_rule.py
+++ b/addons/stock/models/stock_rule.py
@@ -44,10 +44,13 @@ class StockRule(models.Model):
route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade')
procure_method = fields.Selection([
('make_to_stock', 'Take From Stock'),
- ('make_to_order', 'Trigger Another Rule')], string='Move Supply Method',
+ ('make_to_order', 'Trigger Another Rule'),
+ ('mts_else_mto', 'Take From Stock, if unavailable, Trigger Another Rule')], string='Move Supply Method',
default='make_to_stock', required=True,
- help="""Create Procurement: A procurement will be created in the source location and the system will try to find a rule to resolve it. The available stock will be ignored.
- Take from Stock: The products will be taken from the available stock.""")
+ help="Take From Stock: the products will be taken from the available stock of the source location.\n"
+ "Trigger Another Rule: the system will try to find a stock rule to bring the products in the source location. The available stock will be ignored.\n"
+ "Take From Stock, if Unavailable, Trigger Another Rule: the products will be taken from the available stock of the source location."
+ "If there is no stock available, the system will try to find a rule to bring the products in the source location.")
route_sequence = fields.Integer('Route Sequence', related='route_id.sequence', store=True, readonly=False)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type',
@@ -109,6 +112,8 @@ class StockRule(models.Model):
suffix = ""
if self.procure_method == 'make_to_order' and self.location_src_id:
suffix = _("
A need is created in %s and a rule will be triggered to fulfill it.") % (source)
+ if self.procure_method == 'mts_else_mto' and self.location_src_id:
+ suffix = _("
If the products are not available in %s, a rule will be triggered to bring products in this location.") % source
message_dict = {
'pull': _('When products are needed in %s,
%s are created from %s to fulfill the need.') % (destination, operation, source) + suffix,
'push': _('When products arrive in %s,
%s are created to send them in %s.') % (source, operation, destination)
@@ -170,12 +175,42 @@ class StockRule(models.Model):
@api.model
def _run_pull(self, procurements):
moves_values_by_company = defaultdict(list)
+ mtso_products_by_locations = defaultdict(list)
+
+ # To handle the `mts_else_mto` procure method, we do a preliminary loop to
+ # isolate the products we would need to read the forecasted quantity,
+ # in order to to batch the read. We also make a sanitary check on the
+ # `location_src_id` field.
for procurement, rule in procurements:
if not rule.location_src_id:
msg = _('No source location defined on stock rule: %s!') % (rule.name, )
raise UserError(msg)
- moves_values_by_company[procurement.company_id.id].append(rule._get_stock_move_values(*procurement))
+ if rule.procure_method == 'mts_else_mto':
+ mtso_products_by_locations[rule.location_src_id].append(procurement.product_id.id)
+
+ # Get the forecasted quantity for the `mts_else_mto` procurement.
+ forecasted_qties_by_loc = {}
+ for location, product_ids in mtso_products_by_locations.items():
+ products = self.env['product.product'].browse(product_ids).with_context(location=location.id)
+ forecasted_qties_by_loc[location] = {product.id: product.virtual_available for product in products}
+
+ # Prepare the move values, adapt the `procure_method` if needed.
+ for procurement, rule in procurements:
+ procure_method = rule.procure_method
+ if rule.procure_method == 'mts_else_mto':
+ qty_needed = procurement.product_uom._compute_quantity(procurement.product_qty, procurement.product_id.uom_id)
+ qty_available = forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id]
+ if float_compare(qty_needed, qty_available, precision_rounding=procurement.product_id.uom_id.rounding) <= 0:
+ procure_method = 'make_to_stock'
+ forecasted_qties_by_loc[rule.location_src_id][procurement.product_id.id] -= qty_needed
+ else:
+ procure_method = 'make_to_order'
+
+ move_values = rule._get_stock_move_values(*procurement)
+ move_values['procure_method'] = procure_method
+ moves_values_by_company[procurement.company_id.id].append(move_values)
+
for company_id, moves_values in moves_values_by_company.items():
# create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
moves = self.env['stock.move'].sudo().with_context(force_company=company_id).create(moves_values)
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 5534dd8f566..b2f9a934ff9 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -335,7 +335,7 @@ class Warehouse(models.Model):
'depends': ['delivery_steps'],
'create_values': {
'active': True,
- 'procure_method': 'make_to_order',
+ 'procure_method': 'mts_else_mto',
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py
index a62ff9fef98..982cddc5195 100644
--- a/addons/stock/tests/test_move2.py
+++ b/addons/stock/tests/test_move2.py
@@ -2049,3 +2049,197 @@ class TestRoutes(TestStockCommon):
pushed_move = move1.move_dest_ids
self.assertEqual(pushed_move.location_dest_id.id, push_location.id)
+
+ def test_mtso_mto(self):
+ """ Run a procurement for 5 products when there are only 4 in stock then
+ check that MTO is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.env.ref('base.res_partner_1')
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
+
+ # We set quantities in the stock location to avoid warnings
+ # triggered by '_onchange_product_id_check_availability'
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.lot_stock_id, 4.0)
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ values = {'warehouse_id': warehouse}
+ rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
+ rule.procure_method = 'mts_else_mto'
+
+ pg = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mto'})
+
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 5.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mto',
+ 'test_mtso_mto',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ )
+ ])
+
+ qty_available = self.env['stock.quant']._get_available_quantity(product_a, warehouse.wh_output_stock_loc_id)
+
+ # 3 pickings should be created.
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', pg.id)])
+ self.assertEquals(len(picking_ids), 3)
+ for picking in picking_ids:
+ # Only the picking from Stock to Pack should be MTS
+ if picking.location_id == warehouse.lot_stock_id:
+ self.assertEquals(picking.move_lines.procure_method, 'make_to_stock')
+ else:
+ self.assertEquals(picking.move_lines.procure_method, 'make_to_order')
+
+ self.assertEquals(len(picking.move_lines), 1)
+ self.assertEquals(picking.move_lines.product_uom_qty, 5, 'The quantity of the move should be the same as on the SO')
+ self.assertEqual(qty_available, 4, 'The 4 products should still be available')
+
+ def test_mtso_mts(self):
+ """ Run a procurement for 4 products when there are 4 in stock then
+ check that MTS is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.env.ref('base.res_partner_1')
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ values = {'warehouse_id': warehouse}
+ rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
+ rule.procure_method = 'mts_else_mto'
+
+ pg = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts'})
+
+ self.env['procurement.group'].run([
+ pg.Procurement(
+ product_a,
+ 4.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts',
+ 'test_mtso_mts',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg
+ }
+ )
+ ])
+
+ # A picking should be created with its move having MTS as procure method.
+ picking_ids = self.env['stock.picking'].search([('group_id', '=', pg.id)])
+ self.assertEquals(len(picking_ids), 1)
+ picking = picking_ids
+ self.assertEquals(picking.move_lines.procure_method, 'make_to_stock')
+ self.assertEquals(len(picking.move_lines), 1)
+ self.assertEquals(picking.move_lines.product_uom_qty, 4)
+
+ def test_mtso_multi_pg(self):
+ """ Run 3 procurements for 2 products at the same times when there are 4 in stock then
+ check that MTS is applied on the moves when the rule is set to 'mts_else_mto'
+ """
+ warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
+ warehouse.delivery_steps = 'pick_pack_ship'
+ partner_demo_customer = self.env.ref('base.res_partner_1')
+ final_location = partner_demo_customer.property_stock_customer
+ product_a = self.env['product.product'].create({
+ 'name': 'ProductA',
+ 'type': 'product',
+ })
+
+ self.env['stock.quant']._update_available_quantity(product_a, warehouse.wh_output_stock_loc_id, 4.0)
+
+ # We alter one rule and we set it to 'mts_else_mto'
+ values = {'warehouse_id': warehouse}
+ rule = self.env['procurement.group']._get_rule(product_a, final_location, values)
+ rule.procure_method = 'mts_else_mto'
+
+ pg1 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-1'})
+ pg2 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-2'})
+ pg3 = self.env['procurement.group'].create({'name': 'Test-pg-mtso-mts-3'})
+
+ self.env['procurement.group'].run([
+ pg1.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts_1',
+ 'test_mtso_mts_1',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg1
+ }
+ ),
+ pg2.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts_2',
+ 'test_mtso_mts_2',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg2
+ }
+ ),
+ pg3.Procurement(
+ product_a,
+ 2.0,
+ product_a.uom_id,
+ final_location,
+ 'test_mtso_mts_3',
+ 'test_mtso_mts_3',
+ warehouse.company_id,
+ {
+ 'warehouse_id': warehouse,
+ 'group_id': pg3
+ }
+ )
+ ])
+
+ pickings_pg1 = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
+ pickings_pg2 = self.env['stock.picking'].search([('group_id', '=', pg2.id)])
+ pickings_pg3 = self.env['stock.picking'].search([('group_id', '=', pg3.id)])
+
+ # The 2 first procurements should have create only 1 picking since enough quantities
+ # are left in the delivery location
+ self.assertEquals(len(pickings_pg1), 1)
+ self.assertEquals(len(pickings_pg2), 1)
+ self.assertEquals(pickings_pg1.move_lines.procure_method, 'make_to_stock')
+ self.assertEquals(pickings_pg2.move_lines.procure_method, 'make_to_stock')
+
+ # The last one should have 3 pickings as there's nothing left in the delivery location
+ self.assertEquals(len(pickings_pg3), 3)
+ for picking in pickings_pg3:
+ # Only the picking from Stock to Pack should be MTS
+ if picking.location_id == warehouse.lot_stock_id:
+ self.assertEquals(picking.move_lines.procure_method, 'make_to_stock')
+ else:
+ self.assertEquals(picking.move_lines.procure_method, 'make_to_order')
+
+ # All the moves should be should have the same quantity as it is on each procurements
+ self.assertEquals(len(picking.move_lines), 1)
+ self.assertEquals(picking.move_lines.product_uom_qty, 2)