[IMP] stock: Add hybrid procure method 'mts_else_mto'

In some occasions, products can be available in a location,
but are still ignored when the MTO is set on those products.
(e.g.: When a product is returned)

To avoid this, we add a `procure_method` named `mts_else_mto`,
allowing procurement to reserve the products if enough of them
are available in his source location.
In order to achieve this,  we apply the MTS or the MTO `procure_method`
on the moves from the procurement depending on whether or not there are
enough quantities of the needed products in the source location
('All or nothing' logic).

Quick performance test:

With a Sale Order of 800 differents products set as MTO+Buy,
Half of these products available in stock.
Before the patch :
2m30s to confirm the SO (800 PO Lines created)

After the patch:
MTO (procure method MTO):
2m30s to confirm the SO (800 PO Lines created)
MTO (procure method MTS_ELSE_MTO):
2m26s to confirm the SO (400 PO Lines created)

TaskID: 1831347

closes odoo/odoo#32778

Signed-off-by: Simon Lejeune (sle) <sle@openerp.com>
This commit is contained in:
Arnaud Baes
2019-04-24 13:22:50 +00:00
parent eda806a40e
commit 1a727ed5fc
3 changed files with 234 additions and 5 deletions
+39 -4
View File
@@ -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 = _("<br>A need is created in <b>%s</b> and a rule will be triggered to fulfill it.") % (source)
if self.procure_method == 'mts_else_mto' and self.location_src_id:
suffix = _("<br>If the products are not available in <b>%s</b>, a rule will be triggered to bring products in this location.") % source
message_dict = {
'pull': _('When products are needed in <b>%s</b>, <br/> <b>%s</b> are created from <b>%s</b> to fulfill the need.') % (destination, operation, source) + suffix,
'push': _('When products arrive in <b>%s</b>, <br/> <b>%s</b> are created to send them in <b>%s</b>.') % (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)
+1 -1
View File
@@ -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',
+194
View File
@@ -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)