From 1e160dffd375ef5bb5ceb7ec34ee172944719b03 Mon Sep 17 00:00:00 2001 From: Arnold Moyaux Date: Wed, 27 Jun 2018 10:19:18 +0200 Subject: [PATCH] [IMP] stock, mrp: picking before and after manufacturing Currently MRP do not handle multi locations for components. In order to be able to use both feature at the same time, we could: - Improve mrp in order to handle multiple stock.move.line for a componenet stock.move - Add a rule that allow to bring all the good from multiple location to a single location. (was already possible but it requires some configuration) This commit introduce a checkbox on the warehouse in order to automatically configure all the routes/rules/locations/picking_types,... necessary to bring all the components to a single location before running the manufacturing order. Technically it also refactor some stock_warehouse.py methods in order to easily override the picking_type, rules and routes creation. Thank to Hetashree Chauhan for his help. task_id: 27785 --- addons/mrp/__init__.py | 12 + addons/mrp/__manifest__.py | 1 + addons/mrp/data/mrp_data.xml | 8 - addons/mrp/models/mrp_production.py | 32 +- addons/mrp/models/stock_rule.py | 2 +- addons/mrp/models/stock_warehouse.py | 298 ++++++--- addons/mrp/tests/__init__.py | 1 + addons/mrp/tests/test_bom.py | 5 +- addons/mrp/tests/test_stock.py | 2 +- .../test_warehouse_multistep_manufacturing.py | 169 +++++ addons/mrp/views/mrp_production_views.xml | 3 + addons/mrp/views/stock_warehouse_views.xml | 20 +- addons/mrp_byproduct/models/mrp_production.py | 2 + addons/purchase_stock/__init__.py | 2 +- addons/purchase_stock/models/stock.py | 68 +- addons/purchase_stock/views/stock_views.xml | 3 + addons/sale_mrp/tests/__init__.py | 1 + .../tests/test_multistep_manufacturing.py | 87 +++ addons/sale_stock/models/sale_order.py | 4 +- addons/sale_stock/models/stock.py | 11 +- addons/stock/models/stock_rule.py | 12 +- addons/stock/models/stock_warehouse.py | 614 +++++++++++------- addons/stock/tests/test_warehouse.py | 1 - addons/stock/views/stock_warehouse_views.xml | 5 +- 24 files changed, 955 insertions(+), 408 deletions(-) create mode 100644 addons/mrp/tests/test_warehouse_multistep_manufacturing.py create mode 100644 addons/sale_mrp/tests/test_multistep_manufacturing.py diff --git a/addons/mrp/__init__.py b/addons/mrp/__init__.py index f17ad3dbaec..5879cc956c9 100644 --- a/addons/mrp/__init__.py +++ b/addons/mrp/__init__.py @@ -4,3 +4,15 @@ from . import models from . import wizard from . import report + +from odoo import api, SUPERUSER_ID + +def _create_warehouse_data(cr, registry): + """ This hook is used to add a default manufacture_pull_id, manufacture + picking_type on every warehouse. It is necessary if the mrp module is + installed after some warehouses were already created. + """ + env = api.Environment(cr, SUPERUSER_ID, {}) + warehouse_ids = env['stock.warehouse'].search([('manufacture_pull_id', '=', False)]) + for warehouse_id in warehouse_ids: + warehouse_id.write({'manufacture_to_resupply': True}) diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index ef0f7b83442..b5bc531c3a6 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -46,4 +46,5 @@ ], 'test': [], 'application': True, + 'post_init_hook': '_create_warehouse_data', } diff --git a/addons/mrp/data/mrp_data.xml b/addons/mrp/data/mrp_data.xml index e8341ce3f52..0792225d707 100644 --- a/addons/mrp/data/mrp_data.xml +++ b/addons/mrp/data/mrp_data.xml @@ -83,14 +83,6 @@ 0 - - - - diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 35b18f29866..1dff9598347 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -164,6 +164,31 @@ class MrpProduction(models.Model): is_locked = fields.Boolean('Is Locked', default=True, copy=False) show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots') production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production') + picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order') + delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') + + @api.depends('procurement_group_id') + def _compute_picking_ids(self): + for order in self: + order.picking_ids = self.env['stock.picking'].search([ + ('group_id', '=', order.procurement_group_id.id), + ]) + order.delivery_count = len(order.picking_ids) + + def action_view_mo_delivery(self): + """ This function returns an action that display picking related to + manufacturing order orders. It can either be a in a list or in a form + view, if there is only one picking to show. + """ + self.ensure_one() + action = self.env.ref('stock.action_picking_tree_all').read()[0] + pickings = self.mapped('picking_ids') + if len(pickings) > 1: + action['domain'] = [('id', 'in', pickings.ids)] + elif pickings: + action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = pickings.id + return action @api.depends('product_uom_id', 'product_qty', 'product_id.uom_id') def _compute_product_uom_qty(self): @@ -433,12 +458,12 @@ class MrpProduction(models.Model): @api.multi def _adjust_procure_method(self): try: - mto_route = self.env['stock.warehouse']._get_mto_route() + mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', 'Make To Order') except: mto_route = False for move in self.move_raw_ids: product = move.product_id - routes = product.route_ids + product.route_from_categ_ids + routes = product.route_ids + product.route_from_categ_ids + move.warehouse_id.route_ids # TODO: optimize with read_group? pull = self.env['stock.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id), ('location_id', '=', move.location_dest_id.id), ('action', '!=', 'push')], limit=1) @@ -576,7 +601,8 @@ class MrpProduction(models.Model): finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) (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}) return True diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 3ff8c0a581c..6b31506afe0 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -66,7 +66,7 @@ class StockRule(models.Model): 'product_id': product_id.id, 'product_qty': product_qty, 'product_uom_id': product_uom.id, - 'location_src_id': self.location_src_id.id or location_id.id, + 'location_src_id': self.location_src_id.id or self.picking_type_id.default_location_src_id.id or location_id.id, 'location_dest_id': location_id.id, 'bom_id': bom.id, 'date_planned_start': fields.Datetime.to_string(self._get_date_planned(product_id, values)), diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py index 6de24962517..1b05a483cc9 100644 --- a/addons/mrp/models/stock_warehouse.py +++ b/addons/mrp/models/stock_warehouse.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, exceptions, fields, models, _ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError class StockWarehouse(models.Model): @@ -12,103 +13,225 @@ class StockWarehouse(models.Model): help="When products are manufactured, they can be manufactured in this warehouse.") manufacture_pull_id = fields.Many2one( 'stock.rule', 'Manufacture Rule') + pbm_mto_pull_id = fields.Many2one( + 'stock.rule', 'Picking Before Manufacturing MTO Rule') + sam_rule_id = fields.Many2one( + 'stock.rule', 'Stock After Manufacturing Rule') manu_type_id = fields.Many2one( 'stock.picking.type', 'Manufacturing Operation Type', domain=[('code', '=', 'mrp_operation')]) - def create_sequences_and_picking_types(self): - res = super(StockWarehouse, self).create_sequences_and_picking_types() - self._create_manufacturing_picking_type() - return res + pbm_type_id = fields.Many2one('stock.picking.type', 'Picking Before Manufacturing Operation Type') + sam_type_id = fields.Many2one('stock.picking.type', 'Stock After Manufacturing Operation Type') - @api.multi - def get_routes_dict(self): - result = super(StockWarehouse, self).get_routes_dict() + manufacture_steps = fields.Selection([ + ('mrp_one_step', 'Manufacture (1 step)'), + ('pbm', 'Pick components and then manufacture (2 steps)'), + ('pbm_sam', 'Pick components, manufacture and then store products (3 steps)')], + 'Manufacture', default='mrp_one_step', required=True, + help="Produce : Move the raw materials to the production location\ + directly and start the manufacturing process.\nPick / Produce : Unload\ + the raw materials from the Stock to Input location first, and then\ + transfer it to the Production location.") + + pbm_route_id = fields.Many2one('stock.location.route', 'Picking Before Manufacturing Route', ondelete='restrict') + + pbm_loc_id = fields.Many2one('stock.location', 'Picking before Manufacturing Location') + sam_loc_id = fields.Many2one('stock.location', 'Stock after Manufacturing Location') + + def get_rules_dict(self): + result = super(StockWarehouse, self).get_rules_dict() + production_location_id = self._get_production_location() for warehouse in self: - result[warehouse.id]['manufacture'] = [self.Routing(warehouse.lot_stock_id, warehouse.lot_stock_id, warehouse.int_type_id, 'manufacture')] + result[warehouse.id].update({ + 'mrp_one_step': [], + 'pbm': [ + self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'), + self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'), + ], + 'pbm_sam': [ + self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'), + self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'), + ], + }) return result - def _get_manufacture_route_id(self): - manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture', raise_if_not_found=False) - if not manufacture_route: - manufacture_route = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1) - if not manufacture_route: - raise exceptions.UserError(_('Can\'t find any generic Manufacture route.')) - return manufacture_route.id + @api.model + def _get_production_location(self): + location = self.env.ref('stock.location_production', raise_if_not_found=False) + if not location: + location = self.env['stock.location'].search([('usage', '=', 'production')], limit=1) + if not location: + raise UserError(_('Can\'t find any production location.')) + return location - def _get_manufacture_pull_rules_values(self, route_values): - if not self.manu_type_id: - self._create_manufacturing_picking_type() - pull_rules_list = self._get_rule_values(route_values, values={ - 'name': self._format_routename(_(' Manufacture')), - 'location_src_id': False, # TDE FIXME - 'action': 'manufacture', - 'route_id': self._get_manufacture_route_id(), - 'picking_type_id': self.manu_type_id.id, - 'propagate': False, - 'active': True}) - return pull_rules_list + def _get_routes_values(self): + routes = super(StockWarehouse, self)._get_routes_values() + routes.update({ + 'pbm_route_id': { + 'routing_key': self.manufacture_steps, + 'depends': ['manufacture_steps', 'manufacture_to_resupply'], + 'route_update_values': { + 'name': self._format_routename(route_type=self.manufacture_steps), + 'active': self.manufacture_steps != 'mrp_one_step', + }, + 'route_create_values': { + 'product_categ_selectable': True, + 'warehouse_selectable': True, + 'product_selectable': False, + 'company_id': self.company_id.id, + 'sequence': 10, + }, + 'rules_values': { + 'active': True, + } + } + }) + return routes - def _create_manufacturing_picking_type(self): - # TDE CLEANME - picking_type_obj = self.env['stock.picking.type'] - seq_obj = self.env['ir.sequence'] - for warehouse in self: - #man_seq_id = seq_obj.sudo().create('name': warehouse.name + _(' Sequence Manufacturing'), 'prefix': warehouse.code + '/MANU/', 'padding') - wh_stock_loc = warehouse.lot_stock_id - seq = seq_obj.search([('code', '=', 'mrp.production')], limit=1) - other_pick_type = picking_type_obj.search([('warehouse_id', '=', warehouse.id)], order = 'sequence desc', limit=1) - color = other_pick_type.color if other_pick_type else 0 - max_sequence = other_pick_type and other_pick_type.sequence or 0 - manu_type = picking_type_obj.create({ + def _get_route_name(self, route_type): + names = { + 'mrp_one_step': _('Manufacture (1 step)'), + 'pbm': _('Pick components and then manufacture'), + 'pbm_sam': _('Pick components, manufacture and then store products (3 steps)'), + } + if route_type in names: + return names[route_type] + else: + return super(StockWarehouse, self)._get_route_name(route_type) + + def _get_global_route_rules_values(self): + rules = super(StockWarehouse, self)._get_global_route_rules_values() + location_id = self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id + rules.update({ + 'manufacture_pull_id': { + 'depends': ['manufacture_steps', 'manufacture_to_resupply'], + 'create_values': { + 'action': 'manufacture', + 'procure_method': 'make_to_order', + 'picking_type_id': self.manu_type_id.id, + 'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', 'Manufacture').id + }, + 'update_values': { + 'active': self.manufacture_to_resupply, + 'name': self._format_rulename(location_id, False, 'Production'), + 'location_id': location_id.id, + } + }, + 'pbm_mto_pull_id': { + 'depends': ['manufacture_steps', 'manufacture_to_resupply'], + 'create_values': { + 'procure_method': 'make_to_order', + 'company_id': self.company_id.id, + 'action': 'pull', + 'auto': 'manual', + 'propagate': True, + 'route_id': self._find_global_route('stock.route_warehouse0_mto', 'Make To Order').id, + 'name': self._format_rulename(self.lot_stock_id, self.pbm_loc_id, 'MTO'), + 'location_id': self.pbm_loc_id.id, + 'location_src_id': self.lot_stock_id.id, + 'picking_type_id': self.pbm_type_id.id + }, + 'update_values': { + 'active': self.manufacture_steps != 'mrp_one_step', + } + }, + # The purpose to move sam rule in the manufacture route instead of + # pbm_route_id is to avoid conflict with receipt in multiple + # step. For example if the product is manufacture and receipt in two + # step it would conflict in WH/Stock since product could come from + # WH/post-prod or WH/input. We do not have this conflict with + # manufacture route since it is set on the product. + 'sam_rule_id': { + 'depends': ['manufacture_steps', 'manufacture_to_resupply'], + 'create_values': { + 'procure_method': 'make_to_order', + 'company_id': self.company_id.id, + 'action': 'pull_push', + 'auto': 'manual', + 'propagate': True, + 'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', 'Manufacture').id, + 'name': self._format_rulename(self.sam_loc_id, self.lot_stock_id, False), + 'location_id': self.lot_stock_id.id, + 'location_src_id': self.sam_loc_id.id, + 'picking_type_id': self.sam_type_id.id + }, + 'update_values': { + 'active': self.manufacture_steps == 'pbm_sam', + } + } + + }) + return rules + + def _get_locations_values(self, vals): + values = super(StockWarehouse, self)._get_locations_values(vals) + def_values = self.default_get(['manufacture_steps']) + manufacture_steps = vals.get('manufacture_steps', def_values['manufacture_steps']) + values.update({ + 'pbm_loc_id': {'name': _('Pre-Production'), 'active': manufacture_steps in ('pbm', 'pbm_sam'), 'usage': 'internal'}, + 'sam_loc_id': {'name': _('Post-Production'), 'active': manufacture_steps == 'pbm_sam', 'usage': 'internal'}, + }) + return values + + def _get_sequence_values(self): + values = super(StockWarehouse, self)._get_sequence_values() + values.update({ + 'pbm_type_id': {'name': self.name + ' ' + _('Sequence picking before manufacturing'), 'prefix': self.code + '/PC/', 'padding': 5}, + 'sam_type_id': {'name': self.name + ' ' + _('Sequence stock after manufacturing'), 'prefix': self.code + '/SFP/', 'padding': 5}, + 'manu_type_id': {'name': self.name + ' ' + _('Sequence production'), 'prefix': self.code + '/MO/', 'padding': 5}, + }) + return values + + def _get_picking_type_create_values(self, max_sequence): + data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence) + data.update({ + 'pbm_type_id': { + 'name': _('Pick Components'), + 'code': 'internal', + 'use_create_lots': True, + 'use_existing_lots': True, + 'default_location_src_id': self.lot_stock_id.id, + 'default_location_dest_id': self.pbm_loc_id.id, + 'sequence': next_sequence + 1 + }, + 'sam_type_id': { + 'name': _('Store Finished Product'), + 'code': 'internal', + 'use_create_lots': True, + 'use_existing_lots': True, + 'default_location_src_id': self.sam_loc_id.id, + 'default_location_dest_id': self.lot_stock_id.id, + 'sequence': next_sequence + 3 + }, + 'manu_type_id': { 'name': _('Manufacturing'), - 'warehouse_id': warehouse.id, 'code': 'mrp_operation', 'use_create_lots': True, - 'use_existing_lots': False, - 'sequence_id': seq.id, - 'default_location_src_id': wh_stock_loc.id, - 'default_location_dest_id': wh_stock_loc.id, - 'sequence': max_sequence, - 'color': color}) - warehouse.write({'manu_type_id': manu_type.id}) + 'use_existing_lots': True, + 'sequence': next_sequence + 2 + }, + }) + return data, max_sequence + 4 - def _create_or_update_manufacture_pull(self, routes_data): - routes_data = routes_data or self.get_routes_dict() - for warehouse in self: - routings = routes_data[warehouse.id]['manufacture'] - if warehouse.manufacture_pull_id: - manufacture_pull = warehouse.manufacture_pull_id - manufacture_pull.write(warehouse._get_manufacture_pull_rules_values(routings)[0]) - else: - manufacture_pull = self.env['stock.rule'].create(warehouse._get_manufacture_pull_rules_values(routings)[0]) - return manufacture_pull - - @api.multi - def create_routes(self): - res = super(StockWarehouse, self).create_routes() - self.ensure_one() - routes_data = self.get_routes_dict() - manufacture_pull = self._create_or_update_manufacture_pull(routes_data) - res['manufacture_pull_id'] = manufacture_pull.id - return res + def _get_picking_type_update_values(self): + data = super(StockWarehouse, self)._get_picking_type_update_values() + data.update({ + 'pbm_type_id': {'active': self.manufacture_to_resupply and self.manufacture_steps in ('pbm', 'pbm_sam')}, + 'sam_type_id': {'active': self.manufacture_to_resupply and self.manufacture_steps == 'pbm_sam'}, + 'manu_type_id': { + 'active': self.manufacture_to_resupply, + 'default_location_src_id': self.manufacture_steps in ('pbm', 'pbm_sam') and self.pbm_loc_id.id or self.lot_stock_id.id, + 'default_location_dest_id': self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id.id, + }, + }) + return data @api.multi def write(self, vals): - if 'manufacture_to_resupply' in vals: - if vals.get("manufacture_to_resupply"): - for warehouse in self.filtered(lambda warehouse: not warehouse.manufacture_pull_id): - manufacture_pull = warehouse._create_or_update_manufacture_pull(self.get_routes_dict()) - vals['manufacture_pull_id'] = manufacture_pull.id - for warehouse in self: - if not warehouse.manu_type_id: - warehouse._create_manufacturing_picking_type() - warehouse.manu_type_id.active = True - else: - for warehouse in self: - if warehouse.manu_type_id: - warehouse.manu_type_id.active = False - self.mapped('manufacture_pull_id').unlink() + if any(field in vals for field in ('manufacture_steps', 'manufacture_to_resupply')): + for warehouse in self: + warehouse._update_location_manufacture(vals.get('manufacture_steps', warehouse.manufacture_steps)) return super(StockWarehouse, self).write(vals) @api.multi @@ -117,6 +240,19 @@ class StockWarehouse(models.Model): routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id') return routes + def _update_location_manufacture(self, new_manufacture_step): + switch_warehouses = self.filtered(lambda wh: wh.manufacture_steps != new_manufacture_step) + loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.pbm_loc_id)) + if loc_warehouse: + loc_warehouse.mapped('pbm_loc_id').write({'active': False}) + loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.sam_loc_id)) + if loc_warehouse: + loc_warehouse.mapped('sam_loc_id').write({'active': False}) + if new_manufacture_step != 'mrp_one_step': + self.mapped('pbm_loc_id').write({'active': True}) + if new_manufacture_step == 'pbm_sam': + self.mapped('sam_loc_id').write({'active': True}) + @api.multi def _update_name_and_code(self, name=False, code=False): res = super(StockWarehouse, self)._update_name_and_code(name, code) diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index ec92be3cee8..ba967b5b60e 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -3,6 +3,7 @@ from . import test_bom from . import test_order from . import test_stock +from . import test_warehouse_multistep_manufacturing from . import test_workorder_operation from . import test_procurement from . import test_unbuild diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index 71421216411..0cd8f9effa4 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -66,7 +66,10 @@ class TestBoM(TestMrpCommon): tmp_picking_type = self.env['stock.picking.type'].create({ 'name': 'Manufacturing', 'code': 'mrp_operation', - 'sequence_id': self.env['ir.sequence'].search([('code', '=', 'mrp.production')], limit=1).id, + 'sequence_id': self.env['ir.sequence'].create({ + 'code': 'mrp.production', + 'name': 'tmp_production_sequence', + }).id, }) test_bom_1 = self.env['mrp.bom'].create({ 'product_tmpl_id': self.product_5.product_tmpl_id.id, diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py index 571890f7271..8169f742ca6 100644 --- a/addons/mrp/tests/test_stock.py +++ b/addons/mrp/tests/test_stock.py @@ -18,7 +18,7 @@ class TestWarehouse(common.TestMrpCommon): warehouse_1_stock_manager.write({ 'manufacture_to_resupply': False }) - self.assertFalse(self.warehouse_1.manufacture_pull_id) + self.assertFalse(self.warehouse_1.manufacture_pull_id.active) self.assertFalse(self.warehouse_1.manu_type_id.active) self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes()) warehouse_1_stock_manager.write({ diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py new file mode 100644 index 00000000000..570ab7e6069 --- /dev/null +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import Form +from odoo.addons.mrp.tests.common import TestMrpCommon + + +class TestMultistepManufacturingWarehouse(TestMrpCommon): + + def setUp(self): + super(TestMultistepManufacturingWarehouse, self).setUp() + # Create warehouse + self.customer_location = self.env['ir.model.data'].xmlid_to_res_id('stock.stock_location_customers') + warehouse_form = Form(self.env['stock.warehouse']) + warehouse_form.name = 'Test Warehouse' + warehouse_form.code = 'TWH' + self.warehouse = warehouse_form.save() + + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Stick' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + product_form.type = 'product' + product_form.route_ids.clear() + product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id) + product_form.route_ids.add(self.warehouse.mto_pull_id.route_id) + self.finished_product = product_form.save() + + # Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Stick' + product_form.type = 'product' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.raw_product = product_form.save() + + # Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.finished_product + bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id + bom_product_form.product_qty = 1.0 + bom_product_form.type = 'normal' + with bom_product_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.raw_product + bom_line.product_qty = 2.0 + + self.bom = bom_product_form.save() + + def _check_location_and_routes(self): + # Check manufacturing pull rule. + self.assertTrue(self.warehouse.manufacture_pull_id) + self.assertTrue(self.warehouse.manufacture_pull_id.active, self.warehouse.manufacture_to_resupply) + self.assertTrue(self.warehouse.manufacture_pull_id.route_id) + # Check new routes created or not. + self.assertTrue(self.warehouse.pbm_route_id) + # Check location should be created and linked to warehouse. + self.assertTrue(self.warehouse.pbm_loc_id) + self.assertEqual(self.warehouse.pbm_loc_id.active, self.warehouse.manufacture_steps != 'mrp_one_step', "Input location must be de-active for single step only.") + self.assertTrue(self.warehouse.manu_type_id.active) + + def test_00_create_warehouse(self): + """ Warehouse testing for direct manufacturing """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'mrp_one_step' + self._check_location_and_routes() + # Check locations of existing pull rule + self.assertFalse(self.warehouse.pbm_route_id.rule_ids, 'only the update of global manufacture route should happen.') + self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id) + + def test_01_warehouse_twostep_manufacturing(self): + """ Warehouse testing for picking before manufacturing """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm' + self._check_location_and_routes() + self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2) + self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id) + + def test_02_warehouse_twostep_manufacturing(self): + """ Warehouse testing for picking ans store after manufacturing """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm_sam' + self._check_location_and_routes() + self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2) + self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.sam_loc_id.id) + + def test_manufacturing_flow(self): + """ Simulate a pick pack ship delivery combined with a picking before + manufacturing and store after manufacturing. Also ensure that the MO and + the moves to stock are created with the generic pull rules. + In order to trigger the rule we create a picking to the customer with + the 'make to order' procure method + """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm_sam' + warehouse.delivery_steps = 'pick_pack_ship' + self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0) + picking_customer = self.env['stock.picking'].create({ + 'location_id': self.warehouse.wh_output_stock_loc_id.id, + 'location_dest_id': self.customer_location, + 'partner_id': self.env['ir.model.data'].xmlid_to_res_id('base.res_partner_4'), + 'picking_type_id': self.warehouse.out_type_id.id, + }) + self.env['stock.move'].create({ + 'name': self.finished_product.name, + 'product_id': self.finished_product.id, + 'product_uom_qty': 2, + 'product_uom': self.uom_unit.id, + 'picking_id': picking_customer.id, + 'location_id': self.warehouse.wh_output_stock_loc_id.id, + 'location_dest_id': self.customer_location, + 'procure_method': 'make_to_order', + 'state': 'draft', + }) + picking_customer.action_confirm() + production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)]) + self.assertTrue(production_order) + + picking_stock_preprod = self.env['stock.move'].search([ + ('product_id', '=', self.raw_product.id), + ('location_id', '=', self.warehouse.lot_stock_id.id), + ('location_dest_id', '=', self.warehouse.pbm_loc_id.id), + ('picking_type_id', '=', self.warehouse.pbm_type_id.id) + ]).picking_id + picking_stock_postprod = self.env['stock.move'].search([ + ('product_id', '=', self.finished_product.id), + ('location_id', '=', self.warehouse.sam_loc_id.id), + ('location_dest_id', '=', self.warehouse.lot_stock_id.id), + ('picking_type_id', '=', self.warehouse.sam_type_id.id) + ]).picking_id + + self.assertTrue(picking_stock_preprod) + self.assertTrue(picking_stock_postprod) + self.assertEqual(picking_stock_preprod.state, 'confirmed') + self.assertEqual(picking_stock_postprod.state, 'waiting') + + picking_stock_preprod.action_assign() + picking_stock_preprod.move_line_ids.qty_done = 4 + picking_stock_preprod.action_done() + + self.assertFalse(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id)) + 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(picking_stock_postprod.state, 'waiting') + + produce_form = Form(self.env['mrp.product.produce'].with_context({ + 'active_id': production_order.id, + 'active_ids': [production_order.id], + })) + produce_form.product_qty = production_order.product_qty + product_produce = produce_form.save() + product_produce.do_produce() + production_order.button_mark_done() + + self.assertFalse(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id)) + + self.assertEqual(picking_stock_postprod.state, 'assigned') + + picking_stock_pick = self.env['stock.move'].search([ + ('product_id', '=', self.finished_product.id), + ('location_id', '=', self.warehouse.lot_stock_id.id), + ('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id), + ('picking_type_id', '=', self.warehouse.pick_type_id.id) + ]).picking_id + self.assertEqual(picking_stock_pick.move_lines.move_orig_ids.picking_id, picking_stock_postprod) diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 63036ccb27e..86399e9a722 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -64,6 +64,9 @@