diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 7682a2c03b1..da17e6777be 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -98,6 +98,34 @@ class MrpProduction(models.Model):
help="The list of operations (list of work centers) to produce the finished product. The routing "
"is mainly used to compute work center costs during operations and to plan future loads on "
"work centers based on production planning.")
+
+ state = fields.Selection([
+ ('confirmed', 'Confirmed'),
+ ('planned', 'Planned'),
+ ('progress', 'In Progress'),
+ ('to_close', 'To Close'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled')], string='State',
+ compute='_compute_state', copy=False, index=True, readonly=True,
+ store=True, track_visibility='onchange',
+ help=" * Confirmed: The MO is confirmed, the stock rules and the reordering of the components are trigerred.\n"
+ " * Planned: The WO are planned.\n"
+ " * In Progress: The production has started (on the MO or on the WO).\n"
+ " * To Close: The production is done, the MO has to be closed.\n"
+ " * Done: The MO is closed, the stock moves are posted. \n"
+ " * Cancelled: The MO has been cancelled, can't be confirmed anymore.")
+ reservation_state = fields.Selection([
+ ('confirmed', 'Waiting'),
+ ('assigned', 'Ready'),
+ ('waiting', 'Waiting Another Operation')],
+ string='Material Availability',
+ compute='_compute_state', copy=False, index=True, readonly=True,
+ store=True, track_visibility='onchange', oldname='availability',
+ help=" * Ready: The material is available to start the production.\n\
+ * Waiting: The material is not available to start the production.\n\
+ The material availability is impacted by the manufacturing readiness\
+ defined on the BoM.")
+
move_raw_ids = fields.One2many(
'stock.move', 'raw_material_production_id', 'Raw Materials', oldname='move_lines',
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
@@ -117,20 +145,6 @@ class MrpProduction(models.Model):
move_dest_ids = fields.One2many('stock.move', 'created_production_id',
string="Stock Movements of Produced Goods")
- state = fields.Selection([
- ('confirmed', 'Confirmed'),
- ('planned', 'Planned'),
- ('progress', 'In Progress'),
- ('done', 'Done'),
- ('cancel', 'Cancelled')], string='State',
- copy=False, default='confirmed', track_visibility='onchange')
- availability = fields.Selection([
- ('assigned', 'Available'),
- ('partially_available', 'Partially Available'),
- ('waiting', 'Waiting'),
- ('none', 'None')], string='Materials Availability',
- compute='_compute_availability', store=True)
-
unreserve_visible = fields.Boolean(
'Allowed to Unreserve Inventory', compute='_compute_unreserve_visible',
help='Technical field to check when we can unreserve')
@@ -147,8 +161,6 @@ class MrpProduction(models.Model):
default=lambda self: self.env['res.company']._company_default_get('mrp.production'),
required=True)
- check_to_done = fields.Boolean(compute="_get_produced_qty", string="Check Produced Qty",
- help="Technical Field to see if we can show 'Mark as Done' button")
qty_produced = fields.Float(compute="_get_produced_qty", string="Quantity Produced")
procurement_group_id = fields.Many2one(
'procurement.group', 'Procurement Group',
@@ -156,7 +168,6 @@ class MrpProduction(models.Model):
propagate = fields.Boolean(
'Propagate cancel and split',
help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too')
- has_moves = fields.Boolean(compute='_has_moves')
scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps')
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
@@ -240,19 +251,51 @@ class MrpProduction(models.Model):
production.workorder_done_count = count_data.get(production.id, 0)
@api.multi
- @api.depends('move_raw_ids.state', 'workorder_ids.move_raw_ids', 'bom_id.ready_to_produce')
- def _compute_availability(self):
- for order in self:
- if not order.move_raw_ids:
- order.availability = 'none'
- continue
- if order.bom_id.ready_to_produce == 'all_available':
- order.availability = any(move.state not in ('assigned', 'done', 'cancel') for move in order.move_raw_ids) and 'waiting' or 'assigned'
+ @api.depends('move_raw_ids.state', 'move_finished_ids.state', 'workorder_ids', 'workorder_ids.state', 'qty_produced', 'move_raw_ids.quantity_done', 'product_qty')
+ def _compute_state(self):
+ """ Compute the production state. It use the same process than stock
+ picking. It exists 3 extra steps for production:
+ - planned: Workorder has been launched (workorders only)
+ - progress: At least one item is produced.
+ - to_close: The quantity produced is greater than the quantity to
+ produce and all work orders has been finished.
+ """
+ # TODO: duplicated code with stock_picking.py
+ for production in self:
+ if not production.move_raw_ids:
+ production.state = 'confirmed'
+ elif all(move.state == 'draft' for move in production.move_raw_ids):
+ production.state = 'confirmed'
+ elif all(move.state == 'cancel' for move in production.move_raw_ids):
+ production.state = 'cancel'
+ elif all(move.state in ['cancel', 'done'] for move in production.move_raw_ids):
+ production.state = 'done'
+ elif production.move_finished_ids.filtered(lambda m: m.state not in ('cancel', 'done') and m.product_id.id == production.product_id.id)\
+ and (production.qty_produced >= production.product_qty)\
+ and (not production.routing_id or all(wo_state in ('cancel', 'done') for wo_state in production.workorder_ids.mapped('state'))):
+ production.state = 'to_close'
+ elif production.workorder_ids and any(wo_state in ('progress') for wo_state in production.workorder_ids.mapped('state'))\
+ or production.qty_produced > 0 and production.qty_produced < production.product_uom_qty:
+ production.state = 'progress'
+ elif production.workorder_ids:
+ production.state = 'planned'
else:
- move_raw_ids = order.move_raw_ids.filtered(lambda m: m.product_qty)
- partial_list = [x.state in ('partially_available', 'assigned') for x in move_raw_ids]
- assigned_list = [x.state in ('assigned', 'done', 'cancel') for x in move_raw_ids]
- order.availability = (all(assigned_list) and 'assigned') or (any(partial_list) and 'partially_available') or 'waiting'
+ production.state = 'confirmed'
+
+ # Compute reservation state
+ # State where the reservation does not matter.
+ if production.state in ('done', 'cancel'):
+ production.reservation_state = False
+ # Compute reservation state according to its component's moves.
+ else:
+ relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves()
+ if relevant_move_state == 'partially_available':
+ if production.routing_id and production.routing_id.operation_ids and production.bom_id.ready_to_produce == 'asap':
+ production.reservation_state = production._get_ready_to_produce_state()
+ else:
+ production.reservation_state = 'confirmed'
+ elif relevant_move_state != 'draft':
+ production.reservation_state = relevant_move_state
@api.depends('move_raw_ids', 'is_locked', 'state', 'move_raw_ids.quantity_done')
def _compute_unreserve_visible(self):
@@ -273,6 +316,9 @@ class MrpProduction(models.Model):
@api.multi
@api.depends('move_raw_ids.quantity_done', 'move_raw_ids.product_uom_qty')
def _compute_consumed_less_than_planned(self):
+ """ Display a warning to the user if a component of the BoM has less
+ quantity than planned.
+ """
for order in self:
order.consumed_less_than_planned = any(order.move_raw_ids.filtered(
lambda move: float_compare(move.quantity_done,
@@ -281,24 +327,14 @@ class MrpProduction(models.Model):
)
@api.multi
- @api.depends('workorder_ids.state', 'move_finished_ids', 'is_locked')
+ @api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked')
def _get_produced_qty(self):
for production in self:
done_moves = production.move_finished_ids.filtered(lambda x: x.state != 'cancel' and x.product_id.id == production.product_id.id)
qty_produced = sum(done_moves.mapped('quantity_done'))
- wo_done = True
- if any([x.state not in ('done', 'cancel') for x in production.workorder_ids]):
- wo_done = False
- production.check_to_done = production.is_locked and done_moves and (qty_produced >= production.product_qty) and (production.state not in ('done', 'cancel')) and wo_done
production.qty_produced = qty_produced
return True
- @api.multi
- @api.depends('move_raw_ids')
- def _has_moves(self):
- for mo in self:
- mo.has_moves = any(mo.move_raw_ids)
-
@api.multi
def _compute_scrap_move_count(self):
data = self.env['stock.scrap'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id'])
@@ -497,6 +533,26 @@ class MrpProduction(models.Model):
move = self._generate_raw_move(bom_line, line_data)
return move, 0, quantity
+ def _get_ready_to_produce_state(self):
+ """ returns 'assigned' if enough components are reserved in order to complete
+ the first operation in the routing. If not returns 'waiting'
+ """
+ self.ensure_one()
+ first_operation = self.routing_id.operation_ids[0]
+ # Get BoM line related to first opeation in rounting. If there is only
+ # one opeation in the routing then it will need all BoM lines.
+ bom_line_ids = self.env['mrp.bom.line']
+ if len(self.routing_id.operation_ids) == 1:
+ bom_line_ids = self.bom_id.bom_line_ids
+ else:
+ bom_line_ids = self.bom_id.bom_line_ids.filtered(lambda bl: bl.operation_id == first_operation)
+ bom_line_ids = bom_line_ids.filtered(lambda bl: not bl._skip_bom_line(self.product_id))
+
+ moves_in_first_operation = self.move_raw_ids.filtered(lambda m: m.bom_line_id in bom_line_ids)
+ if all(move.state == 'assigned' for move in moves_in_first_operation):
+ return 'assigned'
+ return 'confirmed'
+
@api.multi
def action_assign(self):
for production in self:
@@ -517,7 +573,7 @@ class MrpProduction(models.Model):
quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty
boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id)
order._generate_workorders(boms)
- return orders_to_plan.write({'state': 'planned'})
+ return True
@api.multi
def _generate_workorders(self, exploded_boms):
@@ -620,7 +676,7 @@ class MrpProduction(models.Model):
(finish_moves | raw_moves)._action_cancel()
picking_ids = production.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
picking_ids.action_cancel()
- self.write({'state': 'cancel', 'is_locked': True})
+
if documents:
filtered_documents = {}
for (parent, responsible), rendering_context in documents.items():
@@ -673,8 +729,8 @@ class MrpProduction(models.Model):
self.post_inventory()
moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel'))
moves_to_cancel._action_cancel()
- self.write({'state': 'done', 'date_finished': fields.Datetime.now()})
- return self.write({'state': 'done'})
+ self.write({'date_finished': fields.Datetime.now()})
+ return True
@api.multi
def do_unreserve(self):
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 67900cb14cd..b1875b93225 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -40,7 +40,7 @@ class MrpWorkorder(models.Model):
help='Technical: used in views only.')
production_availability = fields.Selection(
'Stock Availability', readonly=True,
- related='production_id.availability', store=True,
+ related='production_id.reservation_state', store=True,
help='Technical: used in views and domains only.')
production_state = fields.Selection(
'Production State', readonly=True,
@@ -428,7 +428,6 @@ class MrpWorkorder(models.Model):
raise UserError(_("You need to define at least one productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
if self.production_id.state != 'progress':
self.production_id.write({
- 'state': 'progress',
'date_start': datetime.now(),
})
timeline.create({
diff --git a/addons/mrp/models/stock_picking.py b/addons/mrp/models/stock_picking.py
index 337d84541ba..c6ad9068b9f 100644
--- a/addons/mrp/models/stock_picking.py
+++ b/addons/mrp/models/stock_picking.py
@@ -20,8 +20,8 @@ class StockPickingType(models.Model):
if not mrp_picking_types:
return
domains = {
- 'count_mo_waiting': [('availability', '=', 'waiting')],
'count_mo_todo': [('state', 'in', ('confirmed', 'planned', 'progress'))],
+ 'count_mo_waiting': [('reservation_state', '=', 'waiting')],
'count_mo_late': [('date_planned_start', '<', fields.Date.today()), ('state', '=', 'confirmed')],
}
for field in domains:
diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py
index e3bd7c3a72e..dd2bed8d08a 100644
--- a/addons/mrp/tests/test_order.py
+++ b/addons/mrp/tests/test_order.py
@@ -195,7 +195,7 @@ class TestMrpOrder(TestMrpCommon):
})
# assign consume material
man_order.action_assign()
- self.assertEqual(man_order.availability, 'waiting', "Production order should be in waiting state.")
+ self.assertEqual(man_order.reservation_state, 'confirmed', "Production order should be in waiting state.")
# check consume materials of manufacturing order
self.assertEqual(len(man_order.move_raw_ids), 4, "Consume material lines are not generated proper.")
@@ -247,7 +247,7 @@ class TestMrpOrder(TestMrpCommon):
man_order.action_assign()
# Check production order status after assign.
- self.assertEqual(man_order.availability, 'assigned', "Production order should be in assigned state.")
+ self.assertEqual(man_order.reservation_state, 'assigned', "Production order should be in assigned state.")
# Plan production order.
man_order.button_plan()
@@ -302,6 +302,7 @@ class TestMrpOrder(TestMrpCommon):
"""
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_5).unlink()
self.bom_3.bom_line_ids.filtered(lambda x: x.product_id == self.product_4).unlink()
+ self.bom_3.ready_to_produce = 'all_available'
production_2 = self.env['mrp.production'].create({
'name': 'MO-Test001',
@@ -313,7 +314,7 @@ class TestMrpOrder(TestMrpCommon):
production_2.action_assign()
# check sub product availability state is waiting
- self.assertEqual(production_2.availability, 'waiting', 'Production order should be availability for waiting state')
+ self.assertEqual(production_2.reservation_state, 'confirmed', 'Production order should be availability for waiting state')
# Update Inventory
inventory_wizard = self.env['stock.change.product.qty'].create({
@@ -324,7 +325,7 @@ class TestMrpOrder(TestMrpCommon):
production_2.action_assign()
# check sub product availability state is partially available
- self.assertEqual(production_2.availability, 'partially_available', 'Production order should be availability for partially available state')
+ self.assertEqual(production_2.reservation_state, 'confirmed', 'Production order should be availability for partially available state')
# Update Inventory
inventory_wizard = self.env['stock.change.product.qty'].create({
@@ -335,7 +336,7 @@ class TestMrpOrder(TestMrpCommon):
production_2.action_assign()
# check sub product availability state is assigned
- self.assertEqual(production_2.availability, 'assigned', 'Production order should be availability for assigned state')
+ self.assertEqual(production_2.reservation_state, 'assigned', 'Production order should be availability for assigned state')
def test_empty_routing(self):
""" Check what happens when you work with an empty routing"""
@@ -409,7 +410,7 @@ class TestMrpOrder(TestMrpCommon):
'bom_id': bom_custom_laptop.id
})
mo_custom_laptop.action_assign()
- self.assertEqual(mo_custom_laptop.availability, 'assigned')
+ self.assertEqual(mo_custom_laptop.reservation_state, 'assigned')
# produce one item, call `post_inventory`
context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id}
diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py
index 859bdf16312..67961b923da 100644
--- a/addons/mrp/tests/test_procurement.py
+++ b/addons/mrp/tests/test_procurement.py
@@ -45,7 +45,7 @@ class TestProcurement(TestMrpCommon):
produce_product_4 = self.env['mrp.production'].search([('product_id', '=', self.product_4.id),
('move_dest_ids', '=', move_raw_product4[0].id)])
# produce product
- self.assertEqual(produce_product_4.availability, 'waiting', "Consume material not available")
+ self.assertEqual(produce_product_4.reservation_state, 'confirmed', "Consume material not available")
# Create production order
# -------------------------
@@ -61,7 +61,7 @@ class TestProcurement(TestMrpCommon):
produce_product_4.action_assign()
self.assertEqual(produce_product_4.product_qty, 8, "Wrong quantity of finish product.")
self.assertEqual(produce_product_4.product_uom_id, self.uom_dozen, "Wrong quantity of finish product.")
- self.assertEqual(produce_product_4.availability, 'assigned', "Consume material not available")
+ self.assertEqual(produce_product_4.reservation_state, 'assigned', "Consume material not available")
# produce product4
# ---------------
@@ -91,7 +91,7 @@ class TestProcurement(TestMrpCommon):
# ------------------------------------
- self.assertEqual(production_product_6.availability, 'assigned', "Consume material not available")
+ self.assertEqual(production_product_6.reservation_state, 'assigned', "Consume material not available")
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production_product_6.id,
'active_ids': [production_product_6.id],
diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py
index 8169f742ca6..b3de05f90bb 100644
--- a/addons/mrp/tests/test_stock.py
+++ b/addons/mrp/tests/test_stock.py
@@ -84,7 +84,7 @@ class TestWarehouse(common.TestMrpCommon):
production_3.action_assign()
# Check Manufacturing order's availability.
- self.assertEqual(production_3.availability, 'assigned', "Production order's availability should be Available.")
+ self.assertEqual(production_3.reservation_state, 'assigned', "Production order's availability should be Available.")
location_id = production_3.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) and production_3.location_src_id.id or production_3.location_dest_id.id,
diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
index bf1195fdaf0..0fd358b0dce 100644
--- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
+++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py
@@ -179,7 +179,7 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon):
self.assertTrue(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
production_order.action_assign()
- self.assertEqual(production_order.availability, 'assigned')
+ self.assertEqual(production_order.reservation_state, 'assigned')
self.assertEqual(picking_stock_postprod.state, 'waiting')
produce_form = Form(self.env['mrp.product.produce'].with_context({
diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py
index 49df9007835..7a70b7e4be9 100644
--- a/addons/mrp/tests/test_workorder_operation.py
+++ b/addons/mrp/tests/test_workorder_operation.py
@@ -288,7 +288,7 @@ class TestWorkOrderProcess(common.TransactionCase):
# Check production order status of availablity
- self.assertEqual(mo_custom_laptop.availability, 'waiting')
+ self.assertEqual(mo_custom_laptop.reservation_state, 'confirmed')
# --------------------------------------------------
# Set inventory for rawmaterial charger and keybord
@@ -321,7 +321,7 @@ class TestWorkOrderProcess(common.TransactionCase):
# Check consumed move status
mo_custom_laptop.action_assign()
- self.assertEqual( mo_custom_laptop.availability, 'assigned')
+ self.assertEqual(mo_custom_laptop.reservation_state, 'assigned')
# Check current status of raw materials.
for move in mo_custom_laptop.move_raw_ids:
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index da95748d529..896a8a8defd 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -14,9 +14,9 @@