[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:
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user