diff options
| author | Arnold Moyaux <arm@odoo.com> | 2018-06-27 10:19:18 +0200 |
|---|---|---|
| committer | Arnold Moyaux <arm@odoo.com> | 2018-08-13 15:38:34 +0200 |
| commit | 1e160dffd375ef5bb5ceb7ec34ee172944719b03 (patch) | |
| tree | dd2b13ed7ecec5b1f786d72030719f771b5645bf | |
| parent | 177981b09f7a765021bfb87401b39875edf91584 (diff) | |
[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 <hch@odoo.com> for his help.
task_id: 27785
24 files changed, 956 insertions, 409 deletions
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 @@ <field name="sequence">0</field> </record> - <function model="ir.model.data" name="_update_xmlids"> - <value model="base" eval="[{ - 'xml_id': 'mrp.picking_type_manufacturing', - 'record': obj().env.ref('stock.warehouse0').manu_type_id, - 'noupdate': True, - }]"/> - </function> - </data> </odoo> 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 - - 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 _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({ + @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_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 _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}) - - 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 + 'use_existing_lots': True, + 'sequence': next_sequence + 2 + }, + }) + return data, max_sequence + 4 - @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 @@ </div> </button> <button name="%(action_mrp_production_moves)d" type="action" string="Inventory Moves" class="oe_stat_button" icon="fa-arrows-v" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/> + <button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" attrs="{'invisible': [('delivery_count', '=', 0)]}" groups="base.group_user"> + <field name="delivery_count" widget="statinfo" string="Picking"/> + </button> <button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}"> <div class="o_field_widget o_stat_info"> <span class="o_stat_value"><field name="scrap_count"/></span> diff --git a/addons/mrp/views/stock_warehouse_views.xml b/addons/mrp/views/stock_warehouse_views.xml index f417265ea0a..f9f2b88202b 100644 --- a/addons/mrp/views/stock_warehouse_views.xml +++ b/addons/mrp/views/stock_warehouse_views.xml @@ -1,17 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <!-- Warehouse --> - <record id="view_warehouse_inherited" model="ir.ui.view"> - <field name="name">Stock Warehouse Inherited</field> + <record id="view_warehouse_inherit_mrp" model="ir.ui.view"> + <field name="name">Stock Warehouse Inherit MRP</field> <field name="model">stock.warehouse</field> <field name="inherit_id" ref="stock.view_warehouse"/> <field name="arch" type="xml"> - <xpath expr="//field[@name='resupply_wh_ids']" position="before"> + <xpath expr="//field[@name='resupply_wh_ids']" position="before"> <field name="manufacture_to_resupply" /> + <field name="manufacture_steps" attrs="{'invisible': [('manufacture_to_resupply', '=', False)]}" widget="radio"/> </xpath> <xpath expr="//field[@name='out_type_id']" position="after"> <field name="manu_type_id" readonly="True"/> </xpath> + <xpath expr="//group[@name='group_resupply']" position="attributes"> + <attribute name="attrs">{}</attribute> + </xpath> + <xpath expr="//field[@name='wh_output_stock_loc_id']" position="after"> + <field name="sam_loc_id"/> + <field name="pbm_loc_id"/> + </xpath> + <xpath expr="//field[@name='out_type_id']" position="after"> + <field name="sam_type_id"/> + <field name="pbm_type_id"/> + </xpath> </field> </record> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/mrp_byproduct/models/mrp_production.py b/addons/mrp_byproduct/models/mrp_production.py index e89bdd855a9..d3d8fbc369e 100644 --- a/addons/mrp_byproduct/models/mrp_production.py +++ b/addons/mrp_byproduct/models/mrp_production.py @@ -30,6 +30,8 @@ class MrpProduction(models.Model): 'warehouse_id': production.location_dest_id.get_warehouse().id, 'origin': production.name, 'unit_factor': qty1 / (production.product_qty - production.qty_produced), + 'propagate': self.propagate, + 'group_id': self.move_dest_ids and self.move_dest_ids.mapped('group_id')[0].id or self.procurement_group_id.id, 'subproduct_id': sub_product.id } move = Move.create(data) diff --git a/addons/purchase_stock/__init__.py b/addons/purchase_stock/__init__.py index 73fc67341cd..6fee2cb6173 100644 --- a/addons/purchase_stock/__init__.py +++ b/addons/purchase_stock/__init__.py @@ -15,4 +15,4 @@ def _create_buy_rules(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) warehouse_ids = env['stock.warehouse'].search([('buy_pull_id', '=', False)]) for warehouse_id in warehouse_ids: - warehouse_id.buy_pull_id = warehouse_id._create_buy_rule() + warehouse_id._create_or_update_global_routes_rules() diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py index 2b2a696a825..f4dbe1baa25 100644 --- a/addons/purchase_stock/models/stock.py +++ b/addons/purchase_stock/models/stock.py @@ -115,43 +115,25 @@ class StockWarehouse(models.Model): help="When products are bought, they can be delivered to this warehouse") buy_pull_id = fields.Many2one('stock.rule', 'Buy rule') - @api.multi - def _get_buy_pull_rule(self): - buy_route_id = self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False) - if not buy_route_id: - buy_route_id = self.env['stock.location.route'].search([('name', 'like', _('Buy'))], limit=1) - if not buy_route_id: - raise UserError(_("Can't find any Buy route. Please create a route with the 'Buy' action for your receipts operation types.")) - - return { - 'name': self._format_routename(_(' Buy')), - 'location_id': self.in_type_id.default_location_dest_id.id, - 'route_id': buy_route_id.id, - 'action': 'buy', - 'picking_type_id': self.in_type_id.id, - 'warehouse_id': self.id, - 'group_propagation_option': 'none', - } - - @api.multi - def create_routes(self): - res = super(StockWarehouse, self).create_routes() # super applies ensure_one() - if self.buy_to_resupply: - res['buy_pull_id'] = self._create_buy_rule() - return res - - @api.multi - def write(self, vals): - if 'buy_to_resupply' in vals: - if vals.get("buy_to_resupply"): - for warehouse in self: - if not warehouse.buy_pull_id: - vals['buy_pull_id'] = self._create_buy_rule() - else: - for warehouse in self: - if warehouse.buy_pull_id: - warehouse.buy_pull_id.unlink() - return super(StockWarehouse, self).write(vals) + def _get_global_route_rules_values(self): + rules = super(StockWarehouse, self)._get_global_route_rules_values() + location_id = self.in_type_id.default_location_dest_id + rules.update({ + 'buy_pull_id': { + 'depends': ['reception_steps', 'buy_to_resupply'], + 'create_values': { + 'action': 'buy', + 'picking_type_id': self.in_type_id.id, + 'route_id': self._find_global_route('purchase.route_warehouse0_buy', 'Buy').id + }, + 'update_values': { + 'active': self.buy_to_resupply, + 'name': self._format_rulename(location_id, False, 'Buy'), + 'location_id': location_id.id, + } + } + }) + return rules @api.multi def _get_all_routes(self): @@ -168,18 +150,6 @@ class StockWarehouse(models.Model): warehouse.buy_pull_id.write({'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)}) return res - @api.multi - def _update_routes(self): - res = super(StockWarehouse, self)._update_routes() - for warehouse in self: - if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id: - warehouse.buy_pull_id.write({'location_id': warehouse.in_type_id.default_location_dest_id.id}) - return res - - def _create_buy_rule(self): - buy_pull_vals = self._get_buy_pull_rule() - buy_pull = self.env['stock.rule'].create(buy_pull_vals) - return buy_pull.id class ReturnPicking(models.TransientModel): _inherit = "stock.return.picking" diff --git a/addons/purchase_stock/views/stock_views.xml b/addons/purchase_stock/views/stock_views.xml index aa91701b5e5..37cab2f8928 100644 --- a/addons/purchase_stock/views/stock_views.xml +++ b/addons/purchase_stock/views/stock_views.xml @@ -19,6 +19,9 @@ <xpath expr="//field[@name='resupply_wh_ids']" position="before"> <field name="buy_to_resupply" /> </xpath> + <xpath expr="//group[@name='group_resupply']" position="attributes"> + <attribute name="attrs">{}</attribute> + </xpath> </field> </record> diff --git a/addons/sale_mrp/tests/__init__.py b/addons/sale_mrp/tests/__init__.py index 8582dd02767..f0acb254e5a 100644 --- a/addons/sale_mrp/tests/__init__.py +++ b/addons/sale_mrp/tests/__init__.py @@ -4,3 +4,4 @@ from . import test_sale_mrp_flow from . import test_sale_mrp_lead_time from . import test_sale_mrp_procurement +from . import test_multistep_manufacturing
\ No newline at end of file diff --git a/addons/sale_mrp/tests/test_multistep_manufacturing.py b/addons/sale_mrp/tests/test_multistep_manufacturing.py new file mode 100644 index 00000000000..73bb601f72a --- /dev/null +++ b/addons/sale_mrp/tests/test_multistep_manufacturing.py @@ -0,0 +1,87 @@ +# -*- 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 TestMultistepManufacturing(TestMrpCommon): + + def setUp(self): + super(TestMultistepManufacturing, self).setUp() + + self.MrpProduction = self.env['mrp.production'] + # Create warehouse + warehouse_form = Form(self.env['stock.warehouse']) + warehouse_form.name = 'Test' + warehouse_form.code = 'Test' + 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.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.product_manu = product_form.save() + + # Create raw product for manufactured product + product_form = Form(self.env['product.product']) + product_form.name = 'Raw Stick' + product_form.uom_id = self.uom_unit + product_form.uom_po_id = self.uom_unit + self.product_raw = product_form.save() + + # Create bom for manufactured product + bom_product_form = Form(self.env['mrp.bom']) + bom_product_form.product_id = self.product_manu + bom_product_form.product_tmpl_id = self.product_manu.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.product_raw + bom_line.product_qty = 2.0 + self.bom_prod_manu = bom_product_form.save() + + # Create sale order + sale_form = Form(self.env['sale.order']) + sale_form.partner_id = self.env.ref('base.res_partner_1') + sale_form.picking_policy = 'direct' + sale_form.warehouse_id = self.warehouse + with sale_form.order_line.new() as line: + line.name = self.product_manu.name + line.product_id = self.product_manu + line.product_uom_qty = 1.0 + line.product_uom = self.uom_unit + line.price_unit = 10.0 + self.sale_order = sale_form.save() + + def test_00_manufacturing_step_one(self): + """ Testing for Step-1 """ + # Change steps of manufacturing. + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'mrp_one_step' + # Confirm sale order. + self.sale_order.action_confirm() + # Check all procurements for created sale order + mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)]) + # Get manufactured procurement + self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.lot_stock_id.id, "Source loction does not match.") + self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.") + self.assertEqual(len(mo_procurement), 1, "No Procurement !") + + def test_01_manufacturing_step_two(self): + """ Testing for Step-2 """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm' + self.sale_order.action_confirm() + # Get manufactured procurement + mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)]) + self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.pbm_loc_id.id, "Source loction does not match.") + self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.") + + self.assertEqual(len(mo_procurement), 1, "No Procurement !") diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 890b821ea41..3e612a6dc0c 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -310,7 +310,7 @@ class SaleOrderLine(models.Model): 'date_planned': date_planned, 'route_ids': self.route_id, 'warehouse_id': self.order_id.warehouse_id or False, - 'partner_dest_id': self.order_id.partner_shipping_id + 'partner_id': self.order_id.partner_shipping_id.id, }) for line in self.filtered("order_id.commitment_date"): date_planned = fields.Datetime.from_string(line.order_id.commitment_date) - timedelta(days=line.order_id.company_id.security_lead) @@ -406,7 +406,7 @@ class SaleOrderLine(models.Model): else: mto_route = False 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 UserError: # if route MTO not found in ir_model_data, we treat the product as in MTS pass diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index 6619f9ae9ff..a07a479f8fc 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -45,13 +45,10 @@ class ProcurementGroup(models.Model): class StockRule(models.Model): _inherit = 'stock.rule' - def _get_stock_move_values(self, product_id, product_qty, product_uom, location_id, name, origin, values, group_id): - result = super(StockRule, self)._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id) - if values.get('sale_line_id', False): - result['sale_line_id'] = values['sale_line_id'] - if values.get('partner_dest_id'): - result['partner_id'] = values['partner_dest_id'].id - return result + def _get_custom_move_fields(self): + fields = super(StockRule, self)._get_custom_move_fields() + fields += ['sale_line_id', 'partner_id'] + return fields class StockPicking(models.Model): diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index e229c1a46bf..da0970f0549 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -186,6 +186,12 @@ class StockRule(models.Model): move._action_confirm() return True + def _get_custom_move_fields(self): + """ The purpose of this method is to be override in order to easily add + fields from procurement 'values' argument to move data. + """ + return [] + def _get_stock_move_values(self, product_id, product_qty, product_uom, location_id, name, origin, values, group_id): ''' Returns a dictionary of values that will be used to create a stock move from a procurement. This function assumes that the given procurement has a rule (action == 'pull' or 'pull_push') set on it. @@ -199,7 +205,7 @@ class StockRule(models.Model): # it is possible that we've already got some move done, so check for the done qty and create # a new move with the correct qty qty_left = product_qty - return { + move_values = { 'name': name[:2000], 'company_id': self.company_id.id or self.location_src_id.company_id.id or self.location_id.company_id.id or values['company_id'].id, 'product_id': product_id.id, @@ -221,6 +227,10 @@ class StockRule(models.Model): 'propagate': self.propagate, 'priority': values.get('priority', "1"), } + for field in self._get_custom_move_fields(): + if field in values: + move_values[field] = values.get(field) + return move_values def _log_next_activity(self, product_id, note): existing_activity = self.env['mail.activity'].search([('res_id', '=', product_id.product_tmpl_id.id), ('res_model_id', '=', self.env.ref('product.model_product_template').id), diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index efbe81eb863..d80d23054ed 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -68,6 +68,7 @@ class Warehouse(models.Model): 'stock.location.route', 'supplied_wh_id', 'Resupply Routes', help="Routes will be created for these resupply warehouses and you can select them on products and product categories") warehouse_count = fields.Integer(compute='_compute_warehouse_count') + show_resupply = fields.Boolean(compute="_compute_show_resupply") _sql_constraints = [ ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'), ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'), @@ -78,6 +79,10 @@ class Warehouse(models.Model): for warehouse in self: warehouse.warehouse_count = self.env['stock.warehouse'].search_count([('id', 'not in', warehouse.ids)]) + def _compute_show_resupply(self): + for warehouse in self: + warehouse.show_resupply = warehouse.user_has_groups("stock.group_stock_multi_warehouses") and warehouse.warehouse_count + @api.model def create(self, vals): # create view location for warehouse then create all locations @@ -86,17 +91,8 @@ class Warehouse(models.Model): if vals.get('company_id'): loc_vals['company_id'] = vals.get('company_id') vals['view_location_id'] = self.env['stock.location'].create(loc_vals).id + sub_locations = self._get_locations_values(vals) - def_values = self.default_get(['reception_steps', 'delivery_steps']) - reception_steps = vals.get('reception_steps', def_values['reception_steps']) - delivery_steps = vals.get('delivery_steps', def_values['delivery_steps']) - sub_locations = { - 'lot_stock_id': {'name': _('Stock'), 'active': True, 'usage': 'internal'}, - 'wh_input_stock_loc_id': {'name': _('Input'), 'active': reception_steps != 'one_step', 'usage': 'internal'}, - 'wh_qc_stock_loc_id': {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'usage': 'internal'}, - 'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'}, - 'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'}, - } for field_name, values in sub_locations.items(): values['location_id'] = vals['view_location_id'] if vals.get('company_id'): @@ -106,11 +102,18 @@ class Warehouse(models.Model): # actually create WH warehouse = super(Warehouse, self).create(vals) # create sequences and operation types - new_vals = warehouse.create_sequences_and_picking_types() + new_vals = warehouse._create_or_update_sequences_and_picking_types() warehouse.write(new_vals) # TDE FIXME: use super ? # create routes and push/stock rules - route_vals = warehouse.create_routes() + route_vals = warehouse._create_or_update_route() warehouse.write(route_vals) + + # Update global route with specific warehouse rule. + warehouse._create_or_update_global_routes_rules() + + # create route selectable on the product to resupply the warehouse from another one + warehouse.create_resupply_routes(warehouse.resupply_wh_ids) + # update partner data if partner assigned if vals.get('partner_id'): self._update_partner_data(vals['partner_id'], vals.get('company_id')) @@ -123,7 +126,8 @@ class Warehouse(models.Model): if vals.get('code') or vals.get('name'): warehouses._update_name_and_code(vals.get('name'), vals.get('code')) - # activate and deactivate location according to reception and delivery option + warehouses._create_missing_locations(vals) + if vals.get('reception_steps'): warehouses._update_location_reception(vals['reception_steps']) if vals.get('delivery_steps'): @@ -142,11 +146,25 @@ class Warehouse(models.Model): res = super(Warehouse, self).write(vals) - # check if we need to delete and recreate route - if vals.get('reception_steps') or vals.get('delivery_steps'): - route_vals = warehouses._update_routes() - if route_vals: - self.write(route_vals) + for warehouse in self: + # check if we need to delete and recreate route + depends = [depend for depends in [value['depends'] for value in warehouse._get_routes_values().values()] for depend in depends] + if any(depend in vals for depend in depends): + picking_type_vals = warehouse._create_or_update_sequences_and_picking_types() + if picking_type_vals: + warehouse.write(picking_type_vals) + route_vals = warehouse._create_or_update_route() + if route_vals: + warehouse.write(route_vals) + # Check if a global rule(mto, buy, ...) need to be modify. + # The field that impact those rules are listed in the + # _get_global_route_rules_values method under the key named + # 'depends'. + global_rules = warehouse._get_global_route_rules_values() + depends = [depend for depends in [value['depends'] for value in global_rules.values()] for depend in depends] + if any(rule in vals for rule in global_rules) or\ + any(depend in vals for depend in depends): + warehouse._create_or_update_global_routes_rules() if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): for warehouse in warehouses: @@ -172,100 +190,237 @@ class Warehouse(models.Model): transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc}) - def create_sequences_and_picking_types(self): + def _create_or_update_sequences_and_picking_types(self): + """ Create or update existing picking types for a warehouse. + Pikcing types are stored on the warehouse in a many2one. If the picking + type exist this method will update it. The update values can be found in + the method _get_picking_type_update_values. If the picking type does not + exist it will be created with a new sequence associated to it. + """ + self.ensure_one() IrSequenceSudo = self.env['ir.sequence'].sudo() PickingType = self.env['stock.picking.type'] - input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps) - # choose the next available color for the operation types of this warehouse all_used_colors = [res['color'] for res in PickingType.search_read([('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')] available_colors = [zef for zef in range(0, 12) if zef not in all_used_colors] color = available_colors[0] if available_colors else 0 + warehouse_data = {} + sequence_data = self._get_sequence_values() + # suit for each warehouse: reception, internal, pick, pack, ship - max_sequence = PickingType.search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc') + max_sequence = self.env['stock.picking.type'].search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc') max_sequence = max_sequence and max_sequence[0]['sequence'] or 0 - warehouse_data = {} - sequence_data = self._get_sequence_values() - # tde todo: backport sequence fix - create_data = { - 'in_type_id': { - 'name': _('Receipts'), - 'code': 'incoming', - 'use_create_lots': True, - 'use_existing_lots': False, - 'default_location_src_id': False, - 'sequence': max_sequence + 1, - }, 'out_type_id': { - 'name': _('Delivery Orders'), - 'code': 'outgoing', - 'use_create_lots': False, - 'use_existing_lots': True, - 'default_location_dest_id': False, - 'sequence': max_sequence + 5, - }, 'pack_type_id': { - 'name': _('Pack'), - 'code': 'internal', - 'use_create_lots': False, - 'use_existing_lots': True, - 'default_location_src_id': self.wh_pack_stock_loc_id.id, - 'default_location_dest_id': output_loc.id, - 'sequence': max_sequence + 4, - }, 'pick_type_id': { - 'name': _('Pick'), - 'code': 'internal', - 'use_create_lots': False, - 'use_existing_lots': True, - 'default_location_src_id': self.lot_stock_id.id, - 'sequence': max_sequence + 3, - }, 'int_type_id': { - 'name': _('Internal Transfers'), - 'code': 'internal', - 'use_create_lots': False, - 'use_existing_lots': True, - 'default_location_src_id': self.lot_stock_id.id, - 'default_location_dest_id': self.lot_stock_id.id, - 'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'), - 'sequence': max_sequence + 2, - }, - } - data = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id) - for field_name in data: - data[field_name].update(create_data[field_name]) + data = self._get_picking_type_update_values() + create_data, max_sequence = self._get_picking_type_create_values(max_sequence) for picking_type, values in data.items(): - sequence = IrSequenceSudo.create(sequence_data[picking_type]) - values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id) - warehouse_data[picking_type] = PickingType.create(values).id - PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']}) - PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']}) + if self[picking_type]: + self[picking_type].update(values) + else: + data[picking_type].update(create_data[picking_type]) + sequence = IrSequenceSudo.create(sequence_data[picking_type]) + values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id) + warehouse_data[picking_type] = PickingType.create(values).id + + if 'out_type_id' in warehouse_data: + PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']}) + if 'in_type_id' in warehouse_data: + PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']}) return warehouse_data - def create_routes(self): - self.ensure_one() - routes_data = self.get_routes_dict() - - reception_route = self._create_or_update_reception_route(routes_data) - delivery_route = self._create_or_update_delivery_route(routes_data) - mto_pull = self._create_or_update_mto_pull(routes_data) - crossdock_route = self._create_or_update_crossdock_route(routes_data) + def _create_or_update_global_routes_rules(self): + """ Some rules are not specific to a warehouse(e.g MTO, Buy, ...) + however they contain rule(s) for a specific warehouse. This method will + update the rules contained in global routes in order to make them match + with the wanted reception, delivery,... steps. + """ + for rule_field, rule_details in self._get_global_route_rules_values().items(): + values = rule_details['update_values'] + if self[rule_field]: + self[rule_field].write(values) + else: + values.update(rule_details['create_values']) + values.update({'warehouse_id': self.id}) + self[rule_field] = self.env['stock.rule'].create(values) + return True + + def _find_global_route(self, xml_id, route_name): + """ return a route record set from an xml_id or its name. """ + route = self.env.ref(xml_id, raise_if_not_found=False) + if not route: + route = self.env['stock.location.route'].search([('name', 'like', _(route_name))], limit=1) + if not route: + raise UserError(_('Can\'t find any generic route %s.') % (route_name)) + return route + + def _get_global_route_rules_values(self): + """ Method used by _create_or_update_global_routes_rules. It's + purpose is to return a dict with this format. + key: The rule contained in a global route that have to be create/update + entry a dict with the following values: + -depends: Field that impact the rule. When a field in depends is + write on the warehouse the rule set as key have to be update. + -create_values: values used in order to create the rule if it does + not exist. + -update_values: values used to update the route when a field in + depends is modify on the warehouse. + """ + # We use 0 since routing are order from stock to cust. If the routing + # order is modify, the mto rule will be wrong. + rule = self.get_rules_dict()[self.id][self.delivery_steps] + rule = [r for r in rule if r.from_loc == self.lot_stock_id][0] + location_id = rule.from_loc + location_dest_id = rule.dest_loc + picking_type_id = rule.picking_type + return { + 'mto_pull_id': { + 'depends': ['delivery_steps'], + 'create_values': { + 'active': True, + '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 + }, + 'update_values': { + 'name': self._format_rulename(location_id, location_dest_id, 'MTO'), + 'location_id': location_dest_id.id, + 'location_src_id': location_id.id, + 'picking_type_id': picking_type_id.id, + } + } + } - # create route selectable on the product to resupply the warehouse from another one - self.create_resupply_routes(self.resupply_wh_ids) + def _create_or_update_route(self): + """ Create or update the warehouse's routes. + _get_routes_values method return a dict with: + - route field name (e.g: crossdock_route_id). + - field that trigger an update on the route (key 'depends'). + - routing_key used in order to find rules contained in the route. + - create values. + - update values when a field in depends is modified. + - rules default values. + This method do an iteration on each route returned and update/create + them. In order to update the rules contained in the route it will + use the get_rules_dict that return a dict: + - a receptions/delivery,... step value as key (e.g 'pick_ship') + - a list of routing object that represents the rules needed to + fullfil the pupose of the route. + The routing_key from _get_routes_values is match with the get_rules_dict + key in order to create/update the rules in the route + (_find_existing_rule_or_create method is responsible for this part). + """ + # Create routes and active/create their related rules. + routes = [] + rules_dict = self.get_rules_dict() + for route_field, route_data in self._get_routes_values().items(): + # If the route exists update it + if self[route_field]: + route = self[route_field] + route.write(route_data['route_update_values']) + route.rule_ids.write({'active': False}) + # Create the route + else: + route_data['route_create_values'].update(route_data['route_update_values']) + route = self.env['stock.location.route'].create(route_data['route_create_values']) + self[route_field] = route + # Get rules needed for the route + routing_key = route_data.get('routing_key') + rules = rules_dict[self.id][routing_key] + if 'rules_values' in route_data: + route_data['rules_values'].update({'route_id': route.id}) + else: + route_data['rules_values'] = {'route_id': route.id} + rules_list = self._get_rule_values( + rules, values=route_data['rules_values']) + # Create/Active rules + self._find_existing_rule_or_create(rules_list) + if route_data['route_create_values'].get('warehouse_selectable', False) or route_data['route_update_values'].get('warehouse_selectable', False): + routes.append(self[route_field]) + return { + 'route_ids': [(4, route.id) for route in routes], + } - # return routes and mto stock rule to store on the warehouse + def _get_routes_values(self): + """ Return information in order to update warehouse routes. + - The key is a route field sotred as a Many2one on the warehouse + - This key contains a dict with route values: + - routing_key: a key used in order to match rules from + get_rules_dict function. It would be usefull in order to generate + the route's rules. + - route_create_values: When the Many2one does not exist the route + is created based on values contained in this dict. + - route_update_values: When a field contained in 'depends' key is + modified and the Many2one exist on the warehouse, the route will be + update with the values contained in this dict. + - rules_values: values added to the routing in order to create the + route's rules. + """ return { - 'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route], - 'mto_pull_id': mto_pull.id, - 'reception_route_id': reception_route.id, - 'delivery_route_id': delivery_route.id, - 'crossdock_route_id': crossdock_route.id, + 'reception_route_id': { + 'routing_key': self.reception_steps, + 'depends': ['reception_steps'], + 'route_update_values': { + 'name': self._format_routename(route_type=self.reception_steps) + }, + '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, + 'procure_method': 'make_to_order' + } + }, + 'delivery_route_id': { + 'routing_key': self.delivery_steps, + 'depends': ['delivery_steps'], + 'route_update_values': { + 'name': self._format_routename(route_type=self.delivery_steps) + }, + '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, + } + }, + 'crossdock_route_id': { + 'routing_key': 'crossdock', + 'depends': ['delivery_steps', 'reception_steps'], + 'route_update_values': { + 'name': self._format_routename(route_type='crossdock'), + 'active': self.reception_steps != 'one_step' and self.delivery_steps != 'ship_only' + }, + 'route_create_values': { + 'warehouse_selectable': True, + 'product_selectable': True, + 'product_categ_selectable': True, + 'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step', + 'company_id': self.company_id.id, + 'sequence': 20, + }, + 'rules_values': { + 'active': True, + 'procure_method': 'make_to_order' + } + } } def _find_existing_rule_or_create(self, rules_list): - """ This method will find existing rule or create new one""" + """ This method will find existing rules or create new one. """ for rule_vals in rules_list: existing_rule = self.env['stock.rule'].search([ ('picking_type_id', '=', rule_vals['picking_type_id']), @@ -280,70 +435,36 @@ class Warehouse(models.Model): else: existing_rule.write({'active': True}) - def _create_or_update_reception_route(self, routes_data): - routes_data = routes_data or self.get_routes_dict() - for warehouse in self: - if warehouse.reception_route_id: - reception_route = warehouse.reception_route_id - reception_route.write({'name': warehouse._format_routename(route_type=warehouse.reception_steps)}) - reception_route.rule_ids.write({'active': False}) - else: - warehouse.reception_route_id = reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps)) - # stock rules for reception - routings = routes_data[warehouse.id][warehouse.reception_steps] - rules_list = warehouse._get_rule_values( - routings, values={'active': True, 'procure_method': 'make_to_order', 'route_id': reception_route.id}) - warehouse._find_existing_rule_or_create(rules_list) - return reception_route - - def _create_or_update_delivery_route(self, routes_data): - """ Delivery (MTS) route """ - routes_data = routes_data or self.get_routes_dict() - for warehouse in self: - if warehouse.delivery_route_id: - delivery_route = warehouse.delivery_route_id - delivery_route.write({'name': warehouse._format_routename(route_type=warehouse.delivery_steps)}) - delivery_route.rule_ids.write({'active': False}) - else: - delivery_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.delivery_steps)) - # stock (pull) rules for delivery - routings = routes_data[warehouse.id][warehouse.delivery_steps] - rules_list = warehouse._get_rule_values( - routings, values={'active': True, 'route_id': delivery_route.id}) - warehouse._find_existing_rule_or_create(rules_list) - return delivery_route - - def _create_or_update_mto_pull(self, routes_data): - """ Create MTO stock rule and link it to the generic MTO route """ - routes_data = routes_data or self.get_routes_dict() - Rule = self.env['stock.rule'] - for warehouse in self: - routings = routes_data[warehouse.id][warehouse.delivery_steps] - if warehouse.mto_pull_id: - mto_pull = warehouse.mto_pull_id - mto_pull.write(warehouse._get_mto_rules_values(routings)[0]) - else: - mto_pull = Rule.create(warehouse._get_mto_rules_values(routings)[0]) - return mto_pull + def _get_locations_values(self, vals): + """ Update the warehouse locations. """ + def_values = self.default_get(['reception_steps', 'delivery_steps']) + reception_steps = vals.get('reception_steps', def_values['reception_steps']) + delivery_steps = vals.get('delivery_steps', def_values['delivery_steps']) + sub_locations = { + 'lot_stock_id': {'name': _('Stock'), 'active': True, 'usage': 'internal'}, + 'wh_input_stock_loc_id': {'name': _('Input'), 'active': reception_steps != 'one_step', 'usage': 'internal'}, + 'wh_qc_stock_loc_id': {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'usage': 'internal'}, + 'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'}, + 'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'}, + } + return sub_locations - def _create_or_update_crossdock_route(self, routes_data): - """ Create or update the cross dock operations route, that can be set on - products and product categories """ - routes_data = routes_data or self.get_routes_dict() + def _create_missing_locations(self, vals): + """ It could happen that the user delete a mandatory location or a + module with new locations was installed after some warehouses creation. + In this case, this function will create missing locations in order to + avoid mistakes during picking types and rules creation. + """ + sub_locations = self._get_locations_values(vals) for warehouse in self: - if warehouse.crossdock_route_id: - crossdock_route = warehouse.crossdock_route_id - crossdock_route.write({'active': warehouse.reception_steps != 'one_step' and warehouse.delivery_steps != 'ship_only'}) - else: - crossdock_route = self.env['stock.location.route'].create(warehouse._get_crossdock_route_values()) - # note: fixed cross-dock is logically mto - routings = routes_data[warehouse.id]['crossdock'] - pull_list = warehouse._get_rule_values( - routings, - values={'procure_method': 'make_to_order', 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', 'route_id': crossdock_route.id}) - for rule_vals in pull_list: - self.env['stock.rule'].create(rule_vals) - return crossdock_route + missing_location = {} + for location, location_values in sub_locations.items(): + if not warehouse[location] and location not in vals: + location_values['location_id'] = vals.get('view_location_id', warehouse.view_location_id.id) + location_values['company_id'] = vals.get('company_id', warehouse.company_id.id) + missing_location[location] = self.env['stock.location'].create(location_values).id + if missing_location: + warehouse.write(missing_location) def create_resupply_routes(self, supplier_warehouses): Route = self.env['stock.location.route'] @@ -359,8 +480,11 @@ class Warehouse(models.Model): output_location = supplier_wh.lot_stock_id if supplier_wh.delivery_steps == 'ship_only' else supplier_wh.wh_output_stock_loc_id # Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists) if supplier_wh.delivery_steps == 'ship_only': - Rule.create(supplier_wh._get_mto_rules_values([ - self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')])[0]) + routing = [self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')] + mto_vals = supplier_wh._get_global_route_rules_values().get('mto_pull_id') + values = mto_vals['create_values'] + mto_rule_val = supplier_wh._get_rule_values(routing, values, name_suffix='MTO') + Rule.create(mto_rule_val[0]) inter_wh_route = Route.create(self._get_inter_warehouse_route_values(supplier_wh)) @@ -404,49 +528,33 @@ class Warehouse(models.Model): 'pick_pack_ship': _('Deliver in 3 steps (pick + pack + ship)')} return names[route_type] - def get_routes_dict(self): - # TDE todo: rename me (was get_routes_dict) + def get_rules_dict(self): + """ Define the rules source/destination locations, picking_type and + action needed for each warehouse route configuration. + """ customer_loc, supplier_loc = self._get_partner_locations() - return dict((warehouse.id, { - 'one_step': [], - 'two_steps': [self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')], - 'three_steps': [ - self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id, 'pull_push'), - self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')], - 'crossdock': [ - self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id, 'pull'), - self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], - 'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id, 'pull')], - 'pick_ship': [ - self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id, 'pull'), - self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], - 'pick_pack_ship': [ - self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id, 'pull'), - self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id,'pull'), - self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], - 'company_id': warehouse.company_id.id, - }) for warehouse in self) - - def _get_reception_delivery_route_values(self, route_type): return { - 'name': self._format_routename(route_type=route_type), - 'product_categ_selectable': True, - 'warehouse_selectable': True, - 'product_selectable': False, - 'company_id': self.company_id.id, - 'sequence': 10, + warehouse.id: { + 'one_step': [], + 'two_steps': [self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')], + 'three_steps': [ + self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id, 'pull_push'), + self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')], + 'crossdock': [ + self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id, 'pull'), + self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], + 'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id, 'pull')], + 'pick_ship': [ + self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id, 'pull'), + self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], + 'pick_pack_ship': [ + self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id, 'pull'), + self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id, 'pull'), + self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], + 'company_id': warehouse.company_id.id, + } for warehouse in self } - @api.model - @api.returns('stock.location.route', lambda value: value.id) - def _get_mto_route(self): - mto_route = self.env.ref('stock.route_warehouse0_mto', raise_if_not_found=False) - if not mto_route: - mto_route = self.env['stock.location.route'].search([('name', 'like', _('Make To Order'))], limit=1) - if not mto_route: - raise UserError(_('Can\'t find any generic Make To Order route.')) - return mto_route - def _get_inter_warehouse_route_values(self, supplier_warehouse): return { 'name': _('%s: Supply Product from %s') % (self.name, supplier_warehouse.name), @@ -458,21 +566,9 @@ class Warehouse(models.Model): 'company_id': self.company_id.id, } - def _get_crossdock_route_values(self): - return { - 'name': self._format_routename(route_type='crossdock'), - 'warehouse_selectable': True, - 'product_selectable': True, - 'product_categ_selectable': True, - 'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step', - 'company_id': self.company_id.id, - 'sequence': 20, - } - # Pull / Push tools # ------------------------------------------------------------ - def _get_rule_values(self, route_values, values=None, name_suffix=''): first_rule = True rules_list = [] @@ -484,7 +580,7 @@ class Warehouse(models.Model): 'action': routing.action, 'auto': 'manual', 'picking_type_id': routing.picking_type.id, - 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order', + 'procure_method': first_rule and 'make_to_stock' or 'make_to_order', 'warehouse_id': self.id, 'company_id': self.company_id.id, 'propagate': routing.picking_type != self.pick_type_id, @@ -494,14 +590,6 @@ class Warehouse(models.Model): first_rule = False return rules_list - def _get_mto_rules_values(self, route_values): - mto_route = self._get_mto_route() - rules_list = self._get_rule_values(route_values, values={ - 'route_id': mto_route.id, - 'procure_method': 'make_to_order', - 'active': True}, name_suffix=_('MTO')) - return rules_list - def _get_supply_pull_rules_values(self, route_values, values=None): pull_values = {} pull_values.update(values) @@ -539,7 +627,7 @@ class Warehouse(models.Model): else: # We need to delete all the MTO stock rules, otherwise they risk to be used in the system Rule.search([ - '&', ('route_id', '=', self._get_mto_route().id), + '&', ('route_id', '=', self._find_global_route('stock.route_warehouse0_mto', 'Make To Order').id), ('location_id.usage', '=', 'transit'), ('action', '!=', 'push'), ('location_src_id', '=', self.lot_stock_id.id)]).write({'active': False}) @@ -556,29 +644,6 @@ class Warehouse(models.Model): ('location_src_id.usage', '=', 'transit') ]).write({'location_id': new_location.id}) - def _update_routes(self): - routes_data = self.get_routes_dict() - # change the default source and destination location and (de)activate operation types - self._update_picking_type() - delivery_route = self._create_or_update_delivery_route(routes_data) - reception_route = self._create_or_update_reception_route(routes_data) - crossdock_route = self._create_or_update_crossdock_route(routes_data) - mto_pull = self._create_or_update_mto_pull(routes_data) - - return { - 'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route], - 'mto_pull_id': mto_pull.id, - 'reception_route_id': reception_route.id, - 'delivery_route_id': delivery_route.id, - 'crossdock_route_id': crossdock_route.id, - } - - @api.one - def _update_picking_type(self): - picking_type_values = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id) - for field_name, values in picking_type_values.items(): - self[field_name].write(values) - def _update_name_and_code(self, new_name=False, new_code=False): if new_code: self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code}) @@ -635,19 +700,74 @@ class Warehouse(models.Model): # Misc # ------------------------------------------------------------ - def _get_picking_type_values(self, reception_steps, delivery_steps, pack_stop_location): - input_loc, output_loc = self._get_input_output_locations(reception_steps, delivery_steps) + def _get_picking_type_update_values(self): + """ Return values in order to update the existing picking type when the + warehouse's delivery_steps or reception_steps are modify. + """ + input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps) return { 'in_type_id': {'default_location_dest_id': input_loc.id}, 'out_type_id': {'default_location_src_id': output_loc.id}, 'pick_type_id': { - 'active': delivery_steps != 'ship_only', - 'default_location_dest_id': output_loc.id if delivery_steps == 'pick_ship' else pack_stop_location.id}, - 'pack_type_id': {'active': delivery_steps == 'pick_pack_ship'}, + 'active': self.delivery_steps != 'ship_only', + 'default_location_dest_id': output_loc.id if self.delivery_steps == 'pick_ship' else self.wh_pack_stock_loc_id.id}, + 'pack_type_id': {'active': self.delivery_steps == 'pick_pack_ship'}, 'int_type_id': {}, } + def _get_picking_type_create_values(self, max_sequence): + """ When a warehouse is created this method return the values needed in + order to create the new picking types for this warehouse. Every picking + type are created at the same time than the warehouse howver they are + activated or archived depending the delivery_steps or reception_steps. + """ + input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps) + return { + 'in_type_id': { + 'name': _('Receipts'), + 'code': 'incoming', + 'use_create_lots': True, + 'use_existing_lots': False, + 'default_location_src_id': False, + 'sequence': max_sequence + 1, + }, 'out_type_id': { + 'name': _('Delivery Orders'), + 'code': 'outgoing', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_dest_id': False, + 'sequence': max_sequence + 5, + }, 'pack_type_id': { + 'name': _('Pack'), + 'code': 'internal', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_src_id': self.wh_pack_stock_loc_id.id, + 'default_location_dest_id': output_loc.id, + 'sequence': max_sequence + 4, + }, 'pick_type_id': { + 'name': _('Pick'), + 'code': 'internal', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_src_id': self.lot_stock_id.id, + 'sequence': max_sequence + 3, + }, 'int_type_id': { + 'name': _('Internal Transfers'), + 'code': 'internal', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_src_id': self.lot_stock_id.id, + 'default_location_dest_id': self.lot_stock_id.id, + 'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'), + 'sequence': max_sequence + 2, + }, + }, max_sequence + 6 + def _get_sequence_values(self): + """ Each picking type is created with a sequence. This method returns + the sequence values associated to each picking type. + """ return { 'in_type_id': { 'name': self.name + ' ' + _('Sequence in'), @@ -677,7 +797,9 @@ class Warehouse(models.Model): } def _format_rulename(self, from_loc, dest_loc, suffix): - rulename = '%s: %s → %s' % (self.code, from_loc.name, dest_loc.name) + rulename = '%s: %s' % (self.code, from_loc.name) + if dest_loc: + rulename += ' → %s' % (dest_loc.name) if suffix: rulename += ' (' + suffix + ')' return rulename diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index 92bb2d3598d..88ff79b7c7d 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -332,7 +332,6 @@ class TestWarehouse(TestStockCommon): route_shop_namur = warehouse_shop_namur.resupply_route_ids route_shop_wavre = warehouse_shop_wavre.resupply_route_ids - # The product contains the 2 resupply routes. product = self.env['product.product'].create({ 'name': 'Fakir', diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml index a3f3ca106e9..0dd86df8cba 100644 --- a/addons/stock/views/stock_warehouse_views.xml +++ b/addons/stock/views/stock_warehouse_views.xml @@ -35,9 +35,10 @@ <field name="reception_steps" widget='radio'/> <field name="delivery_steps" widget='radio'/> </group> - <group string="Resupply" groups="stock.group_stock_multi_warehouses"> + <field name="show_resupply" invisible="1"/> + <group name="group_resupply" string="Resupply" attrs="{'invisible': [('show_resupply', '!=', True)]}"> <field name="warehouse_count" invisible="1"/> - <field name="resupply_wh_ids" domain="[('id', '!=', id)]" widget='many2many_checkboxes' attrs="{'invisible': [('warehouse_count', '<', 1)]}"/> + <field name="resupply_wh_ids" attrs="{'invisible': [('show_resupply', '!=', True)]}" domain="[('id', '!=', id)]" widget='many2many_checkboxes'/> </group> </group> </page> |
