summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorTiffany Chang (tic) <tic@odoo.com>2020-08-20 10:53:38 +0000
committerTiffany Chang (tic) <tic@odoo.com>2020-09-09 07:15:25 +0000
commit2688cd1bae489b1dec8862f4de727ac20514844c (patch)
tree100df8853bbd5e5664eedb4187936bbeede4ddad /addons
parentf0da27e7eb474e6db4cd999306c420804aaa6f9b (diff)
[IMP] mrp(_subcontracting), stock: MO reserve and replenish automatically
This commit does 3 things: 1. When 'action_confirm' is triggered in a manufacturing order, any 'move_raw_ids' (components) that are unable to be fully reserved when `action_assign` is triggered will be checked for an automatic reordering rule (RR). If a rule exists it will be auto-triggered. This includes subcontractor manufacturing orders. Relevant test updated to match this. 2. When a MO is completed, auto check if there are corresponding confirmed stock moves that can be populated by the completed products and trigger their 'action_assign' to reserve the completed products. 3. If a new move line is added to an already confirmed MO, then do same auto-RR check/trigger. (note will occur for pickings without extra code since `stock_picking.action_confirm` is triggered again in this case.) This reuses similar logic as odoo/odoo#52433 from task 2244230. Originally this was done in the `action_assign`, but has been moved to `action_confirm` so as to imitate the now archived MTO process with RR. Overlapping logic has been moved into stock_move for easier reusability. Note that this now means editing existing moves' `product_uom_qty` (i.e. Demand) will not have a flow to auto-trigger their RR. Subcontracting test updated to match + new mrp test added for this new feature. closes odoo/odoo#57174 Task: 2322496 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/mrp/models/mrp_production.py8
-rw-r--r--addons/mrp/tests/test_procurement.py158
-rw-r--r--addons/mrp_subcontracting/tests/test_subcontracting.py14
-rw-r--r--addons/stock/models/stock_move.py25
-rw-r--r--addons/stock/models/stock_picking.py23
-rw-r--r--addons/stock/tests/test_proc_rule.py39
6 files changed, 234 insertions, 33 deletions
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 2f704d033c1..732eddabe30 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -978,6 +978,8 @@ class MrpProduction(models.Model):
if moves_to_confirm:
moves_to_confirm._action_confirm()
+ # run scheduler for moves forecasted to not have enough in stock
+ moves_to_confirm._trigger_scheduler()
self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm()
@@ -1076,6 +1078,8 @@ class MrpProduction(models.Model):
production.move_raw_ids._adjust_procure_method()
(production.move_raw_ids | production.move_finished_ids)._action_confirm()
production.workorder_ids._action_confirm()
+ # run scheduler for moves forecasted to not have enough in stock
+ production.move_raw_ids._trigger_scheduler()
return True
def action_assign(self):
@@ -1437,6 +1441,10 @@ class MrpProduction(models.Model):
productions_to_backorder._post_inventory(cancel_backorder=False)
backorders = productions_to_backorder._generate_backorder_productions()
+ # if completed products make other confirmed/partially_available moves available, assign them
+ done_move_finished_ids = (productions_to_backorder.move_finished_ids | productions_not_to_backorder.move_finished_ids).filtered(lambda m: m.state == 'done')
+ done_move_finished_ids._trigger_assign()
+
# Moves without quantity done are not posted => set them as done instead of canceling. In
# case the user edits the MO later on and sets some consumed quantity on those, we do not
# want the move lines to be canceled.
diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py
index ea62c577477..6901920c91d 100644
--- a/addons/mrp/tests/test_procurement.py
+++ b/addons/mrp/tests/test_procurement.py
@@ -346,3 +346,161 @@ class TestProcurement(TestMrpCommon):
move_dest._action_assign()
self.assertEqual(move_dest.reserved_availability, 10.0)
+
+ def test_auto_assign(self):
+ """ When auto reordering rule exists, check for when:
+ 1. There is not enough of a manufactured product to assign (reserve for) a picking => auto-create 1st MO
+ 2. There is not enough of a manufactured component to assign the created MO => auto-create 2nd MO
+ 3. Add an extra manufactured component (not in stock) to 1st MO => auto-create 3rd MO
+ 4. When 2nd MO is completed => auto-assign to 1st MO
+ 5. When 1st MO is completed => auto-assign to picking """
+
+ self.warehouse = self.env.ref('stock.warehouse0')
+ route_manufacture = self.warehouse.manufacture_pull_id.route_id
+
+ product_1 = self.env['product.product'].create({
+ 'name': 'Cake',
+ 'type': 'product',
+ 'route_ids': [(6, 0, [route_manufacture.id])]
+ })
+ product_2 = self.env['product.product'].create({
+ 'name': 'Cake Mix',
+ 'type': 'product',
+ 'route_ids': [(6, 0, [route_manufacture.id])]
+ })
+ product_3 = self.env['product.product'].create({
+ 'name': 'Flour',
+ 'type': 'consu',
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_1.id,
+ 'product_tmpl_id': product_1.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1,
+ 'consumption': 'flexible',
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_2.id, 'product_qty': 1}),
+ ]})
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_2.id,
+ 'product_tmpl_id': product_2.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_3.id, 'product_qty': 1}),
+ ]})
+
+ # extra manufactured component added to 1st MO after it is already confirmed
+ product_4 = self.env['product.product'].create({
+ 'name': 'Flavor Enchancer',
+ 'type': 'product',
+ 'route_ids': [(6, 0, [route_manufacture.id])]
+ })
+ product_5 = self.env['product.product'].create({
+ 'name': 'MSG',
+ 'type': 'consu',
+ })
+
+ self.env['mrp.bom'].create({
+ 'product_id': product_4.id,
+ 'product_tmpl_id': product_4.product_tmpl_id.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'product_qty': 1,
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': product_5.id, 'product_qty': 1}),
+ ]})
+
+ # setup auto orderpoints (reordering rules)
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Cake RR',
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'product_id': product_1.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Cake Mix RR',
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'product_id': product_2.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'Flavor Enchancer RR',
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'product_id': product_4.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
+ # create picking output to trigger creating MO for reordering product_1
+ pick_output = self.env['stock.picking'].create({
+ 'name': 'Cake Delivery Order',
+ 'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'move_lines': [(0, 0, {
+ 'name': '/',
+ 'product_id': product_1.id,
+ 'product_uom': product_1.uom_id.id,
+ 'product_uom_qty': 10.00,
+ 'procure_method': 'make_to_stock',
+ })],
+ })
+ pick_output.action_confirm() # should trigger orderpoint to create and confirm 1st MO
+ pick_output.action_assign()
+
+ mo = self.env['mrp.production'].search([
+ ('product_id', '=', product_1.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertEqual(len(mo), 1, "Manufacture order was not automatically created")
+ mo.action_assign()
+ self.assertEqual(mo.move_raw_ids.reserved_availability, 0, "No components should be reserved yet")
+ self.assertEqual(mo.product_qty, 15, "Quantity to produce should be picking demand + reordering rule max qty")
+
+ # 2nd MO for product_2 should have been created and confirmed when 1st MO for product_1 was confirmed
+ mo2 = self.env['mrp.production'].search([
+ ('product_id', '=', product_2.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertEqual(len(mo2), 1, 'Second manufacture order was not created')
+ self.assertEqual(mo2.product_qty, 20, "Quantity to produce should be MO's 'to consume' qty + reordering rule max qty")
+ mo2_form = Form(mo2)
+ mo2_form.qty_producing = 20
+ mo2 = mo2_form.save()
+ mo2.button_mark_done()
+
+ self.assertEqual(mo.move_raw_ids.reserved_availability, 15, "Components should have been auto-reserved")
+
+ # add new component to 1st MO
+ mo_form = Form(mo)
+ with mo_form.move_raw_ids.new() as line:
+ line.product_id = product_4
+ line.product_uom_qty = 1
+ mo_form.save() # should trigger orderpoint to create and confirm 3rd MO
+
+ mo3 = self.env['mrp.production'].search([
+ ('product_id', '=', product_4.id),
+ ('state', '=', 'confirmed')
+ ])
+
+ self.assertEqual(len(mo3), 1, 'Third manufacture order for added component was not created')
+ self.assertEqual(mo3.product_qty, 6, "Quantity to produce should be 1 + reordering rule max qty")
+
+ mo_form = Form(mo)
+ mo.move_raw_ids.quantity_done = 15
+ mo_form.qty_producing = 15
+ mo = mo_form.save()
+ mo.button_mark_done()
+
+ self.assertEqual(pick_output.move_ids_without_package.reserved_availability, 10, "Completed products should have been auto-reserved in picking")
diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py
index 4c58db4659b..88e21407ff2 100644
--- a/addons/mrp_subcontracting/tests/test_subcontracting.py
+++ b/addons/mrp_subcontracting/tests/test_subcontracting.py
@@ -224,9 +224,8 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
def test_flow_4(self):
""" Tick "Manufacture" and "MTO" on the components and trigger the
creation of the subcontracting manufacturing order through a receipt
- picking. Checks that the delivery to the subcontractor is not created
- at the receipt creation. Then run the scheduler and check that
- the delivery and MO exist.
+ picking. Checks that the delivery and MO for its components are
+ automatically created.
"""
# Tick "manufacture" and MTO on self.comp2
mto_route = self.env.ref('stock.route_warehouse0_mto')
@@ -269,15 +268,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
('location_id', '=', warehouse.lot_stock_id.id),
('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
])
- self.assertFalse(move)
-
- self.env['procurement.group'].run_scheduler(company_id=self.env.company.id)
-
- move = self.env['stock.move'].search([
- ('product_id', '=', self.comp2.id),
- ('location_id', '=', warehouse.lot_stock_id.id),
- ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)
- ])
self.assertTrue(move)
picking_delivery = move.picking_id
self.assertTrue(picking_delivery)
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index ae809c5acf0..98633a4f0c0 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -13,6 +13,7 @@ from dateutil import relativedelta
from odoo import SUPERUSER_ID, _, api, fields, models
from odoo.exceptions import UserError
+from odoo.osv import expression
from odoo.tools.float_utils import float_compare, float_is_zero, float_repr, float_round
from odoo.tools.misc import format_date
@@ -1730,3 +1731,27 @@ class StockMove(models.Model):
def _show_details_in_draft(self):
self.ensure_one()
return self.state != 'draft' or (self.picking_id.immediate_transfer and self.state == 'draft')
+
+ def _trigger_scheduler(self):
+ """ Check for auto-triggered orderpoints and trigger them. """
+ orderpoints_by_company = defaultdict(lambda: self.env['stock.warehouse.orderpoint'])
+ for move in self:
+ orderpoint = self.env['stock.warehouse.orderpoint'].search([
+ ('product_id', '=', move.product_id.id),
+ ('trigger', '=', 'auto'),
+ ('location_id', 'parent_of', move.location_id.id),
+ ('company_id', '=', move.company_id.id)
+ ], limit=1)
+ if orderpoint:
+ orderpoints_by_company[orderpoint.company_id] |= orderpoint
+ for company, orderpoints in orderpoints_by_company.items():
+ orderpoints._procure_orderpoint_confirm(company_id=company, raise_user_error=False)
+
+ def _trigger_assign(self):
+ """ Check for and trigger action_assign for confirmed/partially_available moves related to done moves. """
+ domains = []
+ for move in self:
+ domains.append([('product_id', '=', move.product_id.id), ('location_id', '=', move.location_dest_id.id)])
+ static_domain = [('state', 'in', ['confirmed', 'partially_available']), ('procure_method', '=', 'make_to_stock')]
+ moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]))
+ moves_to_reserve._action_assign()
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 0f495ec0156..cac496480fd 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -694,6 +694,9 @@ class Picking(models.Model):
self.mapped('move_lines')\
.filtered(lambda move: move.state == 'draft')\
._action_confirm()
+
+ # run scheduler for moves forecasted to not have enough in stock
+ self.mapped('move_lines').filtered(lambda move: move.state not in ('draft', 'cancel', 'done'))._trigger_scheduler()
return True
def action_assign(self):
@@ -713,19 +716,6 @@ class Picking(models.Model):
moves._action_assign()
package_level_done.write({'is_done': True})
- # run scheduler for moves are not fully reserved
- orderpoints_by_company = defaultdict(lambda: self.env['stock.warehouse.orderpoint'])
- for move in moves:
- orderpoint = self.env['stock.warehouse.orderpoint'].search([
- ('product_id', '=', move.product_id.id),
- ('trigger', '=', 'auto'),
- ('location_id', 'parent_of', move.location_id.id),
- ], limit=1)
- if orderpoint:
- orderpoints_by_company[move.company_id] |= orderpoint
- for company, orderpoints in orderpoints_by_company.items():
- orderpoints._procure_orderpoint_confirm(company_id=company, raise_user_error=False)
-
return True
def action_cancel(self):
@@ -777,12 +767,7 @@ class Picking(models.Model):
# if incoming moves make other confirmed/partially_available moves available, assign them
done_incoming_moves = self.filtered(lambda p: p.picking_type_id.code == 'incoming').move_lines.filtered(lambda m: m.state == 'done')
- domains = []
- for move in done_incoming_moves:
- domains.append([('product_id', '=', move.product_id.id), ('location_id', '=', move.location_dest_id.id)])
- static_domain = [('state', 'in', ['confirmed', 'partially_available']), ('procure_method', '=', 'make_to_stock')]
- moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]))
- moves_to_reserve._action_assign()
+ done_incoming_moves._trigger_assign()
self._send_confirmation_email()
return True
diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py
index f9885a5a992..8e4b2999aac 100644
--- a/addons/stock/tests/test_proc_rule.py
+++ b/addons/stock/tests/test_proc_rule.py
@@ -149,14 +149,20 @@ class TestProcRule(TransactionCase):
self.assertEqual(receipt_move.product_uom_qty, 17.0)
def test_reordering_rule_2(self):
- """Test when not enough product to assign picking, automatically run
- reordering rule.
+ """Test when there is not enough product to assign a picking => automatically run
+ reordering rule (RR). Add extra product to already confirmed picking => automatically
+ run another RR
"""
self.productA = self.env['product.product'].create({
'name': 'Desk Combination',
'type': 'product',
})
+ self.productB = self.env['product.product'].create({
+ 'name': 'Desk Decoration',
+ 'type': 'product',
+ })
+
warehouse = self.env['stock.warehouse'].search([], limit=1)
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = self.productA
@@ -165,6 +171,14 @@ class TestProcRule(TransactionCase):
orderpoint_form.product_max_qty = 5.0
orderpoint = orderpoint_form.save()
+ self.env['stock.warehouse.orderpoint'].create({
+ 'name': 'ProductB RR',
+ 'location_id': warehouse.lot_stock_id.id,
+ 'product_id': self.productB.id,
+ 'product_min_qty': 0,
+ 'product_max_qty': 5,
+ })
+
self.env['stock.rule'].create({
'name': 'Rule Supplier',
'route_id': warehouse.reception_route_id.id,
@@ -197,10 +211,31 @@ class TestProcRule(TransactionCase):
('product_id', '=', self.productA.id),
('location_id', '=', self.env.ref('stock.stock_location_suppliers').id)
])
+
self.assertTrue(receipt_move)
self.assertEqual(receipt_move.date.date(), date.today())
self.assertEqual(receipt_move.product_uom_qty, 17.0)
+ delivery_picking.write({'move_lines': [(0, 0, {
+ 'name': 'Extra Move',
+ 'product_id': self.productB.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ 'location_id': warehouse.lot_stock_id.id,
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
+ 'picking_id': delivery_picking.id,
+ 'additional': True
+ })]})
+
+ receipt_move2 = self.env['stock.move'].search([
+ ('product_id', '=', self.productB.id),
+ ('location_id', '=', self.env.ref('stock.stock_location_suppliers').id)
+ ])
+
+ self.assertTrue(receipt_move2)
+ self.assertEqual(receipt_move2.date.date(), date.today())
+ self.assertEqual(receipt_move2.product_uom_qty, 10.0)
+
class TestProcRuleLoad(TransactionCase):
def setUp(cls):