From b6ba6038ad8f419fb5ba805c10c987eb3ff93530 Mon Sep 17 00:00:00 2001 From: Arnold Moyaux Date: Thu, 23 Aug 2018 11:33:37 +0000 Subject: [PATCH] [IMP] stock: archive warehouse Archiving a warehouse didn't archive its related locations, routes and rules. So we now archive the related objects and add some constraints to avoid having an inconsistent state in stock with archived warehouse. TASK-ID: 30659 closes odoo/odoo#26555 --- addons/mrp/models/stock_warehouse.py | 13 +-- addons/stock/models/stock_location.py | 11 ++ addons/stock/models/stock_warehouse.py | 88 +++++++++++---- addons/stock/tests/test_warehouse.py | 144 ++++++++++++++++++++++++- 4 files changed, 221 insertions(+), 35 deletions(-) diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py index ad525bd1e73..7496236882b 100644 --- a/addons/mrp/models/stock_warehouse.py +++ b/addons/mrp/models/stock_warehouse.py @@ -242,17 +242,8 @@ class StockWarehouse(models.Model): 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}) + self.mapped('pbm_loc_id').write({'active': new_manufacture_step != 'mrp_one_step'}) + self.mapped('sam_loc_id').write({'active': new_manufacture_step == 'pbm_sam'}) @api.multi def _update_name_and_code(self, name=False, code=False): diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py index 336dbac47bb..6accdc3b01e 100644 --- a/addons/stock/models/stock_location.py +++ b/addons/stock/models/stock_location.py @@ -96,6 +96,17 @@ class Location(models.Model): " location as there are products reserved in this location." " Please unreserve the products first." )) + if 'active' in values: + if not self.env.context.get('do_not_check_quant'): + children_location = self.env['stock.location'].with_context(active_test=False).search([('id', 'child_of', self.ids)]) + internal_children_locations = children_location.filtered(lambda l: l.usage == 'internal') + children_quants = self.env['stock.quant'].search([('quantity', '!=', 0), ('reserved_quantity', '!=', 0), ('location_id', 'in', internal_children_locations.ids)]) + if children_quants and values['active'] == False: + raise UserError(_('You still have some product in locations %s') % + (','.join(children_quants.mapped('location_id.name')))) + else: + super(Location, children_location - self).with_context({'do_not_check_quant': True}).write(values) + return super(Location, self).write(values) def name_get(self): diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index dc270bff609..2e2d171eddf 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -121,7 +121,7 @@ class Warehouse(models.Model): def write(self, vals): Route = self.env['stock.location.route'] - warehouses = self.with_context(active_test=False) # TDE FIXME: check this + warehouses = self.with_context(active_test=False) if vals.get('code') or vals.get('name'): warehouses._update_name_and_code(vals.get('name'), vals.get('code')) @@ -146,7 +146,7 @@ class Warehouse(models.Model): res = super(Warehouse, self).write(vals) - for warehouse in self: + for warehouse in warehouses: # 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): @@ -166,12 +166,66 @@ class Warehouse(models.Model): any(depend in vals for depend in depends): warehouse._create_or_update_global_routes_rules() + if 'active' in vals: + picking_type_ids = self.env['stock.picking.type'].with_context(active_test=False).search([('warehouse_id', '=', warehouse.id)]) + move_ids = self.env['stock.move'].search([ + ('picking_type_id', 'in', picking_type_ids.ids), + ('state', 'not in', ('done', 'cancel')), + ]) + if move_ids: + raise UserError(_('You still have ongoing operations for picking\ + types %s in warehouse %s') % + (', '.join(move_ids.mapped('picking_type_id.name')), warehouse.name)) + else: + picking_type_ids.write({'active': vals['active']}) + location_ids = self.env['stock.location'].with_context(active_test=False).search([('location_id', 'child_of', warehouse.view_location_id.id)]) + picking_type_using_locations = self.env['stock.picking.type'].search([ + ('default_location_src_id', 'in', location_ids.ids), + ('default_location_dest_id', 'in', location_ids.ids), + ('id', 'not in', picking_type_ids.ids), + ]) + if picking_type_using_locations: + raise UserError(_('%s use default source or destination locations\ + from warehouse %s that will be archived.') % + (', '.join(picking_type_using_locations.mapped('name')), warehouse.name)) + warehouse.view_location_id.write({'active': vals['active']}) + + rule_ids = self.env['stock.rule'].with_context(active_test=False).search([('warehouse_id', '=', self.id)]) + # Only modify route that apply on this warehouse. + route_ids = warehouse.route_ids.filtered(lambda r: len(r.warehouse_ids) == 1).write({'active': vals['active']}) + rule_ids.write({'active': vals['active']}) + + if warehouse.active: + # Catch all warehouse fields that trigger a modfication on + # routes, rules, picking types and locations (e.g the reception + # steps). The purpose is to write on it in order to let the + # write method set the correct field to active or archive. + depends = set([]) + for rule_item in warehouse._get_global_route_rules_values().values(): + for depend in rule_item.get('depends', []): + depends.add(depend) + for rule_item in warehouse._get_routes_values().values(): + for depend in rule_item.get('depends', []): + depends.add(depend) + values = {'resupply_route_ids': [(4, route.id) for route in warehouse.resupply_route_ids]} + for depend in depends: + values.update({depend: warehouse[depend]}) + warehouse.write(values) + if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): for warehouse in warehouses: to_add = new_resupply_whs - old_resupply_whs[warehouse.id] to_remove = old_resupply_whs[warehouse.id] - new_resupply_whs if to_add: - warehouse.create_resupply_routes(to_add) + existing_route = Route.search([ + ('supplied_wh_id', '=', warehouse.id), + ('supplier_wh_id', 'in', to_remove.ids), + ('active', '=', False) + ]) + if existing_route: + existing_route.write({'active': True}) + else: + warehouse.create_resupply_routes(to_add) if to_remove: Route.search([('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', 'in', to_remove.ids)]).write({'active': False}) # TDE FIXME: shouldn't we remove stock rules also ? because this could make them global (not sure) @@ -366,7 +420,8 @@ class Warehouse(models.Model): 'routing_key': self.reception_steps, 'depends': ['reception_steps'], 'route_update_values': { - 'name': self._format_routename(route_type=self.reception_steps) + 'name': self._format_routename(route_type=self.reception_steps), + 'active': self.active, }, 'route_create_values': { 'product_categ_selectable': True, @@ -384,7 +439,8 @@ class Warehouse(models.Model): 'routing_key': self.delivery_steps, 'depends': ['delivery_steps'], 'route_update_values': { - 'name': self._format_routename(route_type=self.delivery_steps) + 'name': self._format_routename(route_type=self.delivery_steps), + 'active': self.active, }, 'route_create_values': { 'product_categ_selectable': True, @@ -665,26 +721,12 @@ class Warehouse(models.Model): warehouse.int_type_id.sequence_id.write(sequence_data['int_type_id']) def _update_location_reception(self, new_reception_step): - switch_warehouses = self.filtered(lambda wh: wh.reception_steps != new_reception_step and not wh._location_used(wh.wh_input_stock_loc_id)) - if switch_warehouses: - (switch_warehouses.mapped('wh_input_stock_loc_id') + switch_warehouses.mapped('wh_qc_stock_loc_id')).write({'active': False}) - if new_reception_step == 'three_steps': - self.mapped('wh_qc_stock_loc_id').write({'active': True}) - if new_reception_step != 'one_step': - self.mapped('wh_input_stock_loc_id').write({'active': True}) + self.mapped('wh_qc_stock_loc_id').write({'active': new_reception_step == 'three_steps'}) + self.mapped('wh_input_stock_loc_id').write({'active': new_reception_step != 'one_step'}) def _update_location_delivery(self, new_delivery_step): - switch_warehouses = self.filtered(lambda wh: wh.delivery_steps != new_delivery_step) - loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.wh_output_stock_loc_id)) - if loc_warehouse: - loc_warehouse.mapped('wh_output_stock_loc_id').write({'active': False}) - loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.wh_pack_stock_loc_id)) - if loc_warehouse: - loc_warehouse.mapped('wh_pack_stock_loc_id').write({'active': False}) - if new_delivery_step == 'pick_pack_ship': - self.mapped('wh_pack_stock_loc_id').write({'active': True}) - if new_delivery_step != 'ship_only': - self.mapped('wh_output_stock_loc_id').write({'active': True}) + self.mapped('wh_pack_stock_loc_id').write({'active': new_delivery_step == 'pick_pack_ship'}) + self.mapped('wh_output_stock_loc_id').write({'active': new_delivery_step != 'ship_only'}) def _location_used(self, location): rules = self.env['stock.rule'].search_count([ diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index 8c5d10cd3f0..0b9fe1e6d20 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from odoo.addons.stock.tests.common2 import TestStockCommon - +from odoo.tests import Form class TestWarehouse(TestStockCommon): @@ -431,6 +431,148 @@ class TestWarehouse(TestStockCommon): # Ensure there still no quants in distribution warehouse self.assertEqual(sum(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id).mapped('quantity')), 0) + def test_toggle_active_warehouse_1(self): + """ Basic test that create a warehouse with classic configuration. + Archive it and check that locations, picking types, routes, rules are + correclty active or archive. + """ + wh = Form(self.env['stock.warehouse']) + wh.name = "The attic of Willy" + wh.code = "WIL" + warehouse = wh.save() + + custom_location = Form(self.env['stock.location']) + custom_location.name = "A Trunk" + custom_location.location_id = warehouse.lot_stock_id + custom_location = custom_location.save() + + # Archive warehouse + warehouse.toggle_active() + # Global rule + self.assertFalse(warehouse.mto_pull_id.active) + + # Route + self.assertFalse(warehouse.reception_route_id.active) + self.assertFalse(warehouse.delivery_route_id.active) + + # Location + self.assertFalse(warehouse.lot_stock_id.active) + self.assertFalse(warehouse.wh_input_stock_loc_id.active) + self.assertFalse(warehouse.wh_qc_stock_loc_id.active) + self.assertFalse(warehouse.wh_output_stock_loc_id.active) + self.assertFalse(warehouse.wh_pack_stock_loc_id.active) + self.assertFalse(custom_location.active) + + # Picking Type + self.assertFalse(warehouse.in_type_id.active) + self.assertFalse(warehouse.out_type_id.active) + self.assertFalse(warehouse.int_type_id.active) + self.assertFalse(warehouse.pick_type_id.active) + self.assertFalse(warehouse.pack_type_id.active) + + # Active warehouse + warehouse.toggle_active() + # Global rule + self.assertTrue(warehouse.mto_pull_id.active) + + # Route + self.assertTrue(warehouse.reception_route_id.active) + self.assertTrue(warehouse.delivery_route_id.active) + + # Location + self.assertTrue(warehouse.lot_stock_id.active) + self.assertFalse(warehouse.wh_input_stock_loc_id.active) + self.assertFalse(warehouse.wh_qc_stock_loc_id.active) + self.assertFalse(warehouse.wh_output_stock_loc_id.active) + self.assertFalse(warehouse.wh_pack_stock_loc_id.active) + self.assertTrue(custom_location.active) + + # Picking Type + self.assertTrue(warehouse.in_type_id.active) + self.assertTrue(warehouse.out_type_id.active) + self.assertTrue(warehouse.int_type_id.active) + self.assertFalse(warehouse.pick_type_id.active) + self.assertFalse(warehouse.pack_type_id.active) + + def test_toggle_active_warehouse_2(self): + wh = Form(self.env['stock.warehouse']) + wh.name = "The attic of Willy" + wh.code = "WIL" + wh.reception_steps = "two_steps" + wh.delivery_steps = "pick_pack_ship" + warehouse = wh.save() + + warehouse.resupply_wh_ids = [(6, 0, [self.warehouse_1.id])] + + custom_location = Form(self.env['stock.location']) + custom_location.name = "A Trunk" + custom_location.location_id = warehouse.lot_stock_id + custom_location = custom_location.save() + + # Add a warehouse on the route. + warehouse.reception_route_id.write({ + 'warehouse_ids': [(4, self.warehouse_1.id)] + }) + + route = Form(self.env['stock.location.route']) + route.name = "Stair" + route = route.save() + + route.warehouse_ids = [(6, 0, [warehouse.id, self.warehouse_1.id])] + + # Pre archive a location and a route + warehouse.delivery_route_id.toggle_active() + warehouse.wh_pack_stock_loc_id.toggle_active() + + # Archive warehouse + warehouse.toggle_active() + # Global rule + self.assertFalse(warehouse.mto_pull_id.active) + + # Route + self.assertTrue(warehouse.reception_route_id.active) + self.assertFalse(warehouse.delivery_route_id.active) + self.assertTrue(route.active) + + # Location + self.assertFalse(warehouse.lot_stock_id.active) + self.assertFalse(warehouse.wh_input_stock_loc_id.active) + self.assertFalse(warehouse.wh_qc_stock_loc_id.active) + self.assertFalse(warehouse.wh_output_stock_loc_id.active) + self.assertFalse(warehouse.wh_pack_stock_loc_id.active) + self.assertFalse(custom_location.active) + + # Picking Type + self.assertFalse(warehouse.in_type_id.active) + self.assertFalse(warehouse.out_type_id.active) + self.assertFalse(warehouse.int_type_id.active) + self.assertFalse(warehouse.pick_type_id.active) + self.assertFalse(warehouse.pack_type_id.active) + + # Active warehouse + warehouse.toggle_active() + # Global rule + self.assertTrue(warehouse.mto_pull_id.active) + + # Route + self.assertTrue(warehouse.reception_route_id.active) + self.assertTrue(warehouse.delivery_route_id.active) + + # Location + self.assertTrue(warehouse.lot_stock_id.active) + self.assertTrue(warehouse.wh_input_stock_loc_id.active) + self.assertFalse(warehouse.wh_qc_stock_loc_id.active) + self.assertTrue(warehouse.wh_output_stock_loc_id.active) + self.assertTrue(warehouse.wh_pack_stock_loc_id.active) + self.assertTrue(custom_location.active) + + # Picking Type + self.assertTrue(warehouse.in_type_id.active) + self.assertTrue(warehouse.out_type_id.active) + self.assertTrue(warehouse.int_type_id.active) + self.assertTrue(warehouse.pick_type_id.active) + self.assertTrue(warehouse.pack_type_id.active) + class TestResupply(TestStockCommon): def setUp(self): super(TestResupply, self).setUp()