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 @@ - + @@ -36,17 +36,16 @@
-