1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4import logging 5from collections import namedtuple 6 7from odoo import _, _lt, api, fields, models 8from odoo.exceptions import UserError 9 10_logger = logging.getLogger(__name__) 11 12 13ROUTE_NAMES = { 14 'one_step': _lt('Receive in 1 step (stock)'), 15 'two_steps': _lt('Receive in 2 steps (input + stock)'), 16 'three_steps': _lt('Receive in 3 steps (input + quality + stock)'), 17 'crossdock': _lt('Cross-Dock'), 18 'ship_only': _lt('Deliver in 1 step (ship)'), 19 'pick_ship': _lt('Deliver in 2 steps (pick + ship)'), 20 'pick_pack_ship': _lt('Deliver in 3 steps (pick + pack + ship)'), 21} 22 23 24class Warehouse(models.Model): 25 _name = "stock.warehouse" 26 _description = "Warehouse" 27 _order = 'sequence,id' 28 _check_company_auto = True 29 # namedtuple used in helper methods generating values for routes 30 Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type', 'action']) 31 32 name = fields.Char('Warehouse', index=True, required=True, default=lambda self: self.env.company.name) 33 active = fields.Boolean('Active', default=True) 34 company_id = fields.Many2one( 35 'res.company', 'Company', default=lambda self: self.env.company, 36 index=True, readonly=True, required=True, 37 help='The company is automatically set from your user preferences.') 38 partner_id = fields.Many2one('res.partner', 'Address', default=lambda self: self.env.company.partner_id, check_company=True) 39 view_location_id = fields.Many2one( 40 'stock.location', 'View Location', 41 domain="[('usage', '=', 'view'), ('company_id', '=', company_id)]", 42 required=True, check_company=True) 43 lot_stock_id = fields.Many2one( 44 'stock.location', 'Location Stock', 45 domain="[('usage', '=', 'internal'), ('company_id', '=', company_id)]", 46 required=True, check_company=True) 47 code = fields.Char('Short Name', required=True, size=5, help="Short name used to identify your warehouse") 48 route_ids = fields.Many2many( 49 'stock.location.route', 'stock_route_warehouse', 'warehouse_id', 'route_id', 50 'Routes', 51 domain="[('warehouse_selectable', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", 52 help='Defaults routes through the warehouse', check_company=True) 53 reception_steps = fields.Selection([ 54 ('one_step', 'Receive goods directly (1 step)'), 55 ('two_steps', 'Receive goods in input and then stock (2 steps)'), 56 ('three_steps', 'Receive goods in input, then quality and then stock (3 steps)')], 57 'Incoming Shipments', default='one_step', required=True, 58 help="Default incoming route to follow") 59 delivery_steps = fields.Selection([ 60 ('ship_only', 'Deliver goods directly (1 step)'), 61 ('pick_ship', 'Send goods in output and then deliver (2 steps)'), 62 ('pick_pack_ship', 'Pack goods, send goods in output and then deliver (3 steps)')], 63 'Outgoing Shipments', default='ship_only', required=True, 64 help="Default outgoing route to follow") 65 wh_input_stock_loc_id = fields.Many2one('stock.location', 'Input Location', check_company=True) 66 wh_qc_stock_loc_id = fields.Many2one('stock.location', 'Quality Control Location', check_company=True) 67 wh_output_stock_loc_id = fields.Many2one('stock.location', 'Output Location', check_company=True) 68 wh_pack_stock_loc_id = fields.Many2one('stock.location', 'Packing Location', check_company=True) 69 mto_pull_id = fields.Many2one('stock.rule', 'MTO rule') 70 pick_type_id = fields.Many2one('stock.picking.type', 'Pick Type', check_company=True) 71 pack_type_id = fields.Many2one('stock.picking.type', 'Pack Type', check_company=True) 72 out_type_id = fields.Many2one('stock.picking.type', 'Out Type', check_company=True) 73 in_type_id = fields.Many2one('stock.picking.type', 'In Type', check_company=True) 74 int_type_id = fields.Many2one('stock.picking.type', 'Internal Type', check_company=True) 75 crossdock_route_id = fields.Many2one('stock.location.route', 'Crossdock Route', ondelete='restrict') 76 reception_route_id = fields.Many2one('stock.location.route', 'Receipt Route', ondelete='restrict') 77 delivery_route_id = fields.Many2one('stock.location.route', 'Delivery Route', ondelete='restrict') 78 warehouse_count = fields.Integer(compute='_compute_warehouse_count') 79 resupply_wh_ids = fields.Many2many( 80 'stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 81 'Resupply From', help="Routes will be created automatically to resupply this warehouse from the warehouses ticked") 82 resupply_route_ids = fields.One2many( 83 'stock.location.route', 'supplied_wh_id', 'Resupply Routes', 84 help="Routes will be created for these resupply warehouses and you can select them on products and product categories") 85 show_resupply = fields.Boolean(compute="_compute_show_resupply") 86 sequence = fields.Integer(default=10, 87 help="Gives the sequence of this line when displaying the warehouses.") 88 _sql_constraints = [ 89 ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'), 90 ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'), 91 ] 92 93 @api.onchange('company_id') 94 def _onchange_company_id(self): 95 group_user = self.env.ref('base.group_user') 96 group_stock_multi_warehouses = self.env.ref('stock.group_stock_multi_warehouses') 97 if group_stock_multi_warehouses not in group_user.implied_ids: 98 return { 99 'warning': { 100 'title': _('Warning'), 101 'message': _('Creating a new warehouse will automatically activate the Storage Locations setting') 102 } 103 } 104 105 @api.depends('name') 106 def _compute_warehouse_count(self): 107 for warehouse in self: 108 warehouse.warehouse_count = self.env['stock.warehouse'].search_count([('id', 'not in', warehouse.ids)]) 109 110 def _compute_show_resupply(self): 111 for warehouse in self: 112 warehouse.show_resupply = warehouse.user_has_groups("stock.group_stock_multi_warehouses") and warehouse.warehouse_count 113 114 @api.model 115 def create(self, vals): 116 # create view location for warehouse then create all locations 117 loc_vals = {'name': vals.get('code'), 'usage': 'view', 118 'location_id': self.env.ref('stock.stock_location_locations').id} 119 if vals.get('company_id'): 120 loc_vals['company_id'] = vals.get('company_id') 121 vals['view_location_id'] = self.env['stock.location'].create(loc_vals).id 122 sub_locations = self._get_locations_values(vals) 123 124 for field_name, values in sub_locations.items(): 125 values['location_id'] = vals['view_location_id'] 126 if vals.get('company_id'): 127 values['company_id'] = vals.get('company_id') 128 vals[field_name] = self.env['stock.location'].with_context(active_test=False).create(values).id 129 130 # actually create WH 131 warehouse = super(Warehouse, self).create(vals) 132 # create sequences and operation types 133 new_vals = warehouse._create_or_update_sequences_and_picking_types() 134 warehouse.write(new_vals) # TDE FIXME: use super ? 135 # create routes and push/stock rules 136 route_vals = warehouse._create_or_update_route() 137 warehouse.write(route_vals) 138 139 # Update global route with specific warehouse rule. 140 warehouse._create_or_update_global_routes_rules() 141 142 # create route selectable on the product to resupply the warehouse from another one 143 warehouse.create_resupply_routes(warehouse.resupply_wh_ids) 144 145 # update partner data if partner assigned 146 if vals.get('partner_id'): 147 self._update_partner_data(vals['partner_id'], vals.get('company_id')) 148 149 self._check_multiwarehouse_group() 150 151 return warehouse 152 153 def write(self, vals): 154 if 'company_id' in vals: 155 for warehouse in self: 156 if warehouse.company_id.id != vals['company_id']: 157 raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one.")) 158 159 Route = self.env['stock.location.route'] 160 warehouses = self.with_context(active_test=False) 161 warehouses._create_missing_locations(vals) 162 163 if vals.get('reception_steps'): 164 warehouses._update_location_reception(vals['reception_steps']) 165 if vals.get('delivery_steps'): 166 warehouses._update_location_delivery(vals['delivery_steps']) 167 if vals.get('reception_steps') or vals.get('delivery_steps'): 168 warehouses._update_reception_delivery_resupply(vals.get('reception_steps'), vals.get('delivery_steps')) 169 170 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): 171 new_resupply_whs = self.new({ 172 'resupply_wh_ids': vals['resupply_wh_ids'] 173 }).resupply_wh_ids._origin 174 old_resupply_whs = {warehouse.id: warehouse.resupply_wh_ids for warehouse in warehouses} 175 176 # If another partner assigned 177 if vals.get('partner_id'): 178 warehouses._update_partner_data(vals['partner_id'], vals.get('company_id')) 179 180 res = super(Warehouse, self).write(vals) 181 182 if vals.get('code') or vals.get('name'): 183 warehouses._update_name_and_code(vals.get('name'), vals.get('code')) 184 185 for warehouse in warehouses: 186 # check if we need to delete and recreate route 187 depends = [depend for depends in [value.get('depends', []) for value in warehouse._get_routes_values().values()] for depend in depends] 188 if 'code' in vals or any(depend in vals for depend in depends): 189 picking_type_vals = warehouse._create_or_update_sequences_and_picking_types() 190 if picking_type_vals: 191 warehouse.write(picking_type_vals) 192 if any(depend in vals for depend in depends): 193 route_vals = warehouse._create_or_update_route() 194 if route_vals: 195 warehouse.write(route_vals) 196 # Check if a global rule(mto, buy, ...) need to be modify. 197 # The field that impact those rules are listed in the 198 # _get_global_route_rules_values method under the key named 199 # 'depends'. 200 global_rules = warehouse._get_global_route_rules_values() 201 depends = [depend for depends in [value.get('depends', []) for value in global_rules.values()] for depend in depends] 202 if any(rule in vals for rule in global_rules) or\ 203 any(depend in vals for depend in depends): 204 warehouse._create_or_update_global_routes_rules() 205 206 if 'active' in vals: 207 picking_type_ids = self.env['stock.picking.type'].with_context(active_test=False).search([('warehouse_id', '=', warehouse.id)]) 208 move_ids = self.env['stock.move'].search([ 209 ('picking_type_id', 'in', picking_type_ids.ids), 210 ('state', 'not in', ('done', 'cancel')), 211 ]) 212 if move_ids: 213 raise UserError(_('You still have ongoing operations for picking types %s in warehouse %s') % 214 (', '.join(move_ids.mapped('picking_type_id.name')), warehouse.name)) 215 else: 216 picking_type_ids.write({'active': vals['active']}) 217 location_ids = self.env['stock.location'].with_context(active_test=False).search([('location_id', 'child_of', warehouse.view_location_id.id)]) 218 picking_type_using_locations = self.env['stock.picking.type'].search([ 219 ('default_location_src_id', 'in', location_ids.ids), 220 ('default_location_dest_id', 'in', location_ids.ids), 221 ('id', 'not in', picking_type_ids.ids), 222 ]) 223 if picking_type_using_locations: 224 raise UserError(_('%s use default source or destination locations from warehouse %s that will be archived.') % 225 (', '.join(picking_type_using_locations.mapped('name')), warehouse.name)) 226 warehouse.view_location_id.write({'active': vals['active']}) 227 228 rule_ids = self.env['stock.rule'].with_context(active_test=False).search([('warehouse_id', '=', warehouse.id)]) 229 # Only modify route that apply on this warehouse. 230 warehouse.route_ids.filtered(lambda r: len(r.warehouse_ids) == 1).write({'active': vals['active']}) 231 rule_ids.write({'active': vals['active']}) 232 233 if warehouse.active: 234 # Catch all warehouse fields that trigger a modfication on 235 # routes, rules, picking types and locations (e.g the reception 236 # steps). The purpose is to write on it in order to let the 237 # write method set the correct field to active or archive. 238 depends = set([]) 239 for rule_item in warehouse._get_global_route_rules_values().values(): 240 for depend in rule_item.get('depends', []): 241 depends.add(depend) 242 for rule_item in warehouse._get_routes_values().values(): 243 for depend in rule_item.get('depends', []): 244 depends.add(depend) 245 values = {'resupply_route_ids': [(4, route.id) for route in warehouse.resupply_route_ids]} 246 for depend in depends: 247 values.update({depend: warehouse[depend]}) 248 warehouse.write(values) 249 250 if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): 251 for warehouse in warehouses: 252 to_add = new_resupply_whs - old_resupply_whs[warehouse.id] 253 to_remove = old_resupply_whs[warehouse.id] - new_resupply_whs 254 if to_add: 255 existing_route = Route.search([ 256 ('supplied_wh_id', '=', warehouse.id), 257 ('supplier_wh_id', 'in', to_remove.ids), 258 ('active', '=', False) 259 ]) 260 if existing_route: 261 existing_route.toggle_active() 262 else: 263 warehouse.create_resupply_routes(to_add) 264 if to_remove: 265 to_disable_route_ids = Route.search([ 266 ('supplied_wh_id', '=', warehouse.id), 267 ('supplier_wh_id', 'in', to_remove.ids), 268 ('active', '=', True) 269 ]) 270 to_disable_route_ids.toggle_active() 271 272 if 'active' in vals: 273 self._check_multiwarehouse_group() 274 return res 275 276 def unlink(self): 277 res = super().unlink() 278 self._check_multiwarehouse_group() 279 return res 280 281 def _check_multiwarehouse_group(self): 282 cnt_by_company = self.env['stock.warehouse'].sudo().read_group([('active', '=', True)], ['company_id'], groupby=['company_id']) 283 if cnt_by_company: 284 max_cnt = max(cnt_by_company, key=lambda k: k['company_id_count']) 285 group_user = self.env.ref('base.group_user') 286 group_stock_multi_warehouses = self.env.ref('stock.group_stock_multi_warehouses') 287 if max_cnt['company_id_count'] <= 1 and group_stock_multi_warehouses in group_user.implied_ids: 288 group_user.write({'implied_ids': [(3, group_stock_multi_warehouses.id)]}) 289 group_stock_multi_warehouses.write({'users': [(3, user.id) for user in group_user.users]}) 290 if max_cnt['company_id_count'] > 1 and group_stock_multi_warehouses not in group_user.implied_ids: 291 group_user.write({'implied_ids': [(4, group_stock_multi_warehouses.id), (4, self.env.ref('stock.group_stock_multi_locations').id)]}) 292 293 @api.model 294 def _update_partner_data(self, partner_id, company_id): 295 if not partner_id: 296 return 297 ResCompany = self.env['res.company'] 298 if company_id: 299 transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id 300 self.env['res.partner'].browse(partner_id).with_company(company_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc}) 301 else: 302 transit_loc = self.env.company.internal_transit_location_id.id 303 self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc}) 304 305 def _create_or_update_sequences_and_picking_types(self): 306 """ Create or update existing picking types for a warehouse. 307 Pikcing types are stored on the warehouse in a many2one. If the picking 308 type exist this method will update it. The update values can be found in 309 the method _get_picking_type_update_values. If the picking type does not 310 exist it will be created with a new sequence associated to it. 311 """ 312 self.ensure_one() 313 IrSequenceSudo = self.env['ir.sequence'].sudo() 314 PickingType = self.env['stock.picking.type'] 315 316 # choose the next available color for the operation types of this warehouse 317 all_used_colors = [res['color'] for res in PickingType.search_read([('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')] 318 available_colors = [zef for zef in range(0, 12) if zef not in all_used_colors] 319 color = available_colors[0] if available_colors else 0 320 321 warehouse_data = {} 322 sequence_data = self._get_sequence_values() 323 324 # suit for each warehouse: reception, internal, pick, pack, ship 325 max_sequence = self.env['stock.picking.type'].search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc') 326 max_sequence = max_sequence and max_sequence[0]['sequence'] or 0 327 328 data = self._get_picking_type_update_values() 329 create_data, max_sequence = self._get_picking_type_create_values(max_sequence) 330 331 for picking_type, values in data.items(): 332 if self[picking_type]: 333 self[picking_type].update(values) 334 else: 335 data[picking_type].update(create_data[picking_type]) 336 sequence = IrSequenceSudo.create(sequence_data[picking_type]) 337 values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id) 338 warehouse_data[picking_type] = PickingType.create(values).id 339 340 if 'out_type_id' in warehouse_data: 341 PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data.get('in_type_id', False)}) 342 if 'in_type_id' in warehouse_data: 343 PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data.get('out_type_id', False)}) 344 return warehouse_data 345 346 def _create_or_update_global_routes_rules(self): 347 """ Some rules are not specific to a warehouse(e.g MTO, Buy, ...) 348 however they contain rule(s) for a specific warehouse. This method will 349 update the rules contained in global routes in order to make them match 350 with the wanted reception, delivery,... steps. 351 """ 352 for rule_field, rule_details in self._get_global_route_rules_values().items(): 353 values = rule_details.get('update_values', {}) 354 if self[rule_field]: 355 self[rule_field].write(values) 356 else: 357 values.update(rule_details['create_values']) 358 values.update({'warehouse_id': self.id}) 359 self[rule_field] = self.env['stock.rule'].create(values) 360 return True 361 362 def _find_global_route(self, xml_id, route_name): 363 """ return a route record set from an xml_id or its name. """ 364 route = self.env.ref(xml_id, raise_if_not_found=False) 365 if not route: 366 route = self.env['stock.location.route'].search([('name', 'like', route_name)], limit=1) 367 if not route: 368 raise UserError(_('Can\'t find any generic route %s.') % (route_name)) 369 return route 370 371 def _get_global_route_rules_values(self): 372 """ Method used by _create_or_update_global_routes_rules. It's 373 purpose is to return a dict with this format. 374 key: The rule contained in a global route that have to be create/update 375 entry a dict with the following values: 376 -depends: Field that impact the rule. When a field in depends is 377 write on the warehouse the rule set as key have to be update. 378 -create_values: values used in order to create the rule if it does 379 not exist. 380 -update_values: values used to update the route when a field in 381 depends is modify on the warehouse. 382 """ 383 # We use 0 since routing are order from stock to cust. If the routing 384 # order is modify, the mto rule will be wrong. 385 rule = self.get_rules_dict()[self.id][self.delivery_steps] 386 rule = [r for r in rule if r.from_loc == self.lot_stock_id][0] 387 location_id = rule.from_loc 388 location_dest_id = rule.dest_loc 389 picking_type_id = rule.picking_type 390 return { 391 'mto_pull_id': { 392 'depends': ['delivery_steps'], 393 'create_values': { 394 'active': True, 395 'procure_method': 'mts_else_mto', 396 'company_id': self.company_id.id, 397 'action': 'pull', 398 'auto': 'manual', 399 'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id 400 }, 401 'update_values': { 402 'name': self._format_rulename(location_id, location_dest_id, 'MTO'), 403 'location_id': location_dest_id.id, 404 'location_src_id': location_id.id, 405 'picking_type_id': picking_type_id.id, 406 } 407 } 408 } 409 410 def _create_or_update_route(self): 411 """ Create or update the warehouse's routes. 412 _get_routes_values method return a dict with: 413 - route field name (e.g: crossdock_route_id). 414 - field that trigger an update on the route (key 'depends'). 415 - routing_key used in order to find rules contained in the route. 416 - create values. 417 - update values when a field in depends is modified. 418 - rules default values. 419 This method do an iteration on each route returned and update/create 420 them. In order to update the rules contained in the route it will 421 use the get_rules_dict that return a dict: 422 - a receptions/delivery,... step value as key (e.g 'pick_ship') 423 - a list of routing object that represents the rules needed to 424 fullfil the pupose of the route. 425 The routing_key from _get_routes_values is match with the get_rules_dict 426 key in order to create/update the rules in the route 427 (_find_existing_rule_or_create method is responsible for this part). 428 """ 429 # Create routes and active/create their related rules. 430 routes = [] 431 rules_dict = self.get_rules_dict() 432 for route_field, route_data in self._get_routes_values().items(): 433 # If the route exists update it 434 if self[route_field]: 435 route = self[route_field] 436 if 'route_update_values' in route_data: 437 route.write(route_data['route_update_values']) 438 route.rule_ids.write({'active': False}) 439 # Create the route 440 else: 441 if 'route_update_values' in route_data: 442 route_data['route_create_values'].update(route_data['route_update_values']) 443 route = self.env['stock.location.route'].create(route_data['route_create_values']) 444 self[route_field] = route 445 # Get rules needed for the route 446 routing_key = route_data.get('routing_key') 447 rules = rules_dict[self.id][routing_key] 448 if 'rules_values' in route_data: 449 route_data['rules_values'].update({'route_id': route.id}) 450 else: 451 route_data['rules_values'] = {'route_id': route.id} 452 rules_list = self._get_rule_values( 453 rules, values=route_data['rules_values']) 454 # Create/Active rules 455 self._find_existing_rule_or_create(rules_list) 456 if route_data['route_create_values'].get('warehouse_selectable', False) or route_data['route_update_values'].get('warehouse_selectable', False): 457 routes.append(self[route_field]) 458 return { 459 'route_ids': [(4, route.id) for route in routes], 460 } 461 462 def _get_routes_values(self): 463 """ Return information in order to update warehouse routes. 464 - The key is a route field sotred as a Many2one on the warehouse 465 - This key contains a dict with route values: 466 - routing_key: a key used in order to match rules from 467 get_rules_dict function. It would be usefull in order to generate 468 the route's rules. 469 - route_create_values: When the Many2one does not exist the route 470 is created based on values contained in this dict. 471 - route_update_values: When a field contained in 'depends' key is 472 modified and the Many2one exist on the warehouse, the route will be 473 update with the values contained in this dict. 474 - rules_values: values added to the routing in order to create the 475 route's rules. 476 """ 477 return { 478 'reception_route_id': { 479 'routing_key': self.reception_steps, 480 'depends': ['reception_steps'], 481 'route_update_values': { 482 'name': self._format_routename(route_type=self.reception_steps), 483 'active': self.active, 484 }, 485 'route_create_values': { 486 'product_categ_selectable': True, 487 'warehouse_selectable': True, 488 'product_selectable': False, 489 'company_id': self.company_id.id, 490 'sequence': 9, 491 }, 492 'rules_values': { 493 'active': True, 494 'propagate_cancel': True, 495 } 496 }, 497 'delivery_route_id': { 498 'routing_key': self.delivery_steps, 499 'depends': ['delivery_steps'], 500 'route_update_values': { 501 'name': self._format_routename(route_type=self.delivery_steps), 502 'active': self.active, 503 }, 504 'route_create_values': { 505 'product_categ_selectable': True, 506 'warehouse_selectable': True, 507 'product_selectable': False, 508 'company_id': self.company_id.id, 509 'sequence': 10, 510 }, 511 'rules_values': { 512 'active': True, 513 } 514 }, 515 'crossdock_route_id': { 516 'routing_key': 'crossdock', 517 'depends': ['delivery_steps', 'reception_steps'], 518 'route_update_values': { 519 'name': self._format_routename(route_type='crossdock'), 520 'active': self.reception_steps != 'one_step' and self.delivery_steps != 'ship_only' 521 }, 522 'route_create_values': { 523 'product_selectable': True, 524 'product_categ_selectable': True, 525 'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step', 526 'company_id': self.company_id.id, 527 'sequence': 20, 528 }, 529 'rules_values': { 530 'active': True, 531 'procure_method': 'make_to_order' 532 } 533 } 534 } 535 536 def _get_receive_routes_values(self, installed_depends): 537 """ Return receive route values with 'procure_method': 'make_to_order' 538 in order to update warehouse routes. 539 540 This function has the same receive route values as _get_routes_values with the addition of 541 'procure_method': 'make_to_order' to the 'rules_values'. This is expected to be used by 542 modules that extend stock and add actions that can trigger receive 'make_to_order' rules (i.e. 543 we don't want any of the generated rules by get_rules_dict to default to 'make_to_stock'). 544 Additionally this is expected to be used in conjunction with _get_receive_rules_dict(). 545 546 args: 547 installed_depends - string value of installed (warehouse) boolean to trigger updating of reception route. 548 """ 549 return { 550 'reception_route_id': { 551 'routing_key': self.reception_steps, 552 'depends': ['reception_steps', installed_depends], 553 'route_update_values': { 554 'name': self._format_routename(route_type=self.reception_steps), 555 'active': self.active, 556 }, 557 'route_create_values': { 558 'product_categ_selectable': True, 559 'warehouse_selectable': True, 560 'product_selectable': False, 561 'company_id': self.company_id.id, 562 'sequence': 9, 563 }, 564 'rules_values': { 565 'active': True, 566 'propagate_cancel': True, 567 'procure_method': 'make_to_order', 568 } 569 } 570 } 571 572 def _find_existing_rule_or_create(self, rules_list): 573 """ This method will find existing rules or create new one. """ 574 for rule_vals in rules_list: 575 existing_rule = self.env['stock.rule'].search([ 576 ('picking_type_id', '=', rule_vals['picking_type_id']), 577 ('location_src_id', '=', rule_vals['location_src_id']), 578 ('location_id', '=', rule_vals['location_id']), 579 ('route_id', '=', rule_vals['route_id']), 580 ('action', '=', rule_vals['action']), 581 ('active', '=', False), 582 ]) 583 if not existing_rule: 584 self.env['stock.rule'].create(rule_vals) 585 else: 586 existing_rule.write({'active': True}) 587 588 def _get_locations_values(self, vals, code=False): 589 """ Update the warehouse locations. """ 590 def_values = self.default_get(['reception_steps', 'delivery_steps']) 591 reception_steps = vals.get('reception_steps', def_values['reception_steps']) 592 delivery_steps = vals.get('delivery_steps', def_values['delivery_steps']) 593 code = vals.get('code') or code or '' 594 code = code.replace(' ', '').upper() 595 company_id = vals.get('company_id', self.default_get(['company_id'])['company_id']) 596 sub_locations = { 597 'lot_stock_id': { 598 'name': _('Stock'), 599 'active': True, 600 'usage': 'internal', 601 'barcode': self._valid_barcode(code + '-STOCK', company_id) 602 }, 603 'wh_input_stock_loc_id': { 604 'name': _('Input'), 605 'active': reception_steps != 'one_step', 606 'usage': 'internal', 607 'barcode': self._valid_barcode(code + '-INPUT', company_id) 608 }, 609 'wh_qc_stock_loc_id': { 610 'name': _('Quality Control'), 611 'active': reception_steps == 'three_steps', 612 'usage': 'internal', 613 'barcode': self._valid_barcode(code + '-QUALITY', company_id) 614 }, 615 'wh_output_stock_loc_id': { 616 'name': _('Output'), 617 'active': delivery_steps != 'ship_only', 618 'usage': 'internal', 619 'barcode': self._valid_barcode(code + '-OUTPUT', company_id) 620 }, 621 'wh_pack_stock_loc_id': { 622 'name': _('Packing Zone'), 623 'active': delivery_steps == 'pick_pack_ship', 624 'usage': 'internal', 625 'barcode': self._valid_barcode(code + '-PACKING', company_id) 626 }, 627 } 628 return sub_locations 629 630 def _valid_barcode(self, barcode, company_id): 631 location = self.env['stock.location'].with_context(active_test=False).search([ 632 ('barcode', '=', barcode), 633 ('company_id', '=', company_id) 634 ]) 635 return not location and barcode 636 637 def _create_missing_locations(self, vals): 638 """ It could happen that the user delete a mandatory location or a 639 module with new locations was installed after some warehouses creation. 640 In this case, this function will create missing locations in order to 641 avoid mistakes during picking types and rules creation. 642 """ 643 for warehouse in self: 644 company_id = vals.get('company_id', warehouse.company_id.id) 645 sub_locations = warehouse._get_locations_values(dict(vals, company_id=company_id), warehouse.code) 646 missing_location = {} 647 for location, location_values in sub_locations.items(): 648 if not warehouse[location] and location not in vals: 649 location_values['location_id'] = vals.get('view_location_id', warehouse.view_location_id.id) 650 location_values['company_id'] = company_id 651 missing_location[location] = self.env['stock.location'].create(location_values).id 652 if missing_location: 653 warehouse.write(missing_location) 654 655 def create_resupply_routes(self, supplier_warehouses): 656 Route = self.env['stock.location.route'] 657 Rule = self.env['stock.rule'] 658 659 input_location, output_location = self._get_input_output_locations(self.reception_steps, self.delivery_steps) 660 internal_transit_location, external_transit_location = self._get_transit_locations() 661 662 for supplier_wh in supplier_warehouses: 663 transit_location = internal_transit_location if supplier_wh.company_id == self.company_id else external_transit_location 664 if not transit_location: 665 continue 666 transit_location.active = True 667 output_location = supplier_wh.lot_stock_id if supplier_wh.delivery_steps == 'ship_only' else supplier_wh.wh_output_stock_loc_id 668 # Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists) 669 if supplier_wh.delivery_steps == 'ship_only': 670 routing = [self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')] 671 mto_vals = supplier_wh._get_global_route_rules_values().get('mto_pull_id') 672 values = mto_vals['create_values'] 673 mto_rule_val = supplier_wh._get_rule_values(routing, values, name_suffix='MTO') 674 Rule.create(mto_rule_val[0]) 675 676 inter_wh_route = Route.create(self._get_inter_warehouse_route_values(supplier_wh)) 677 678 pull_rules_list = supplier_wh._get_supply_pull_rules_values( 679 [self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')], 680 values={'route_id': inter_wh_route.id}) 681 pull_rules_list += self._get_supply_pull_rules_values( 682 [self.Routing(transit_location, input_location, self.in_type_id, 'pull')], 683 values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id}) 684 for pull_rule_vals in pull_rules_list: 685 Rule.create(pull_rule_vals) 686 687 # Routing tools 688 # ------------------------------------------------------------ 689 690 def _get_input_output_locations(self, reception_steps, delivery_steps): 691 return (self.lot_stock_id if reception_steps == 'one_step' else self.wh_input_stock_loc_id, 692 self.lot_stock_id if delivery_steps == 'ship_only' else self.wh_output_stock_loc_id) 693 694 def _get_transit_locations(self): 695 return self.company_id.internal_transit_location_id, self.env.ref('stock.stock_location_inter_wh', raise_if_not_found=False) or self.env['stock.location'] 696 697 @api.model 698 def _get_partner_locations(self): 699 ''' returns a tuple made of the browse record of customer location and the browse record of supplier location''' 700 Location = self.env['stock.location'] 701 customer_loc = self.env.ref('stock.stock_location_customers', raise_if_not_found=False) 702 supplier_loc = self.env.ref('stock.stock_location_suppliers', raise_if_not_found=False) 703 if not customer_loc: 704 customer_loc = Location.search([('usage', '=', 'customer')], limit=1) 705 if not supplier_loc: 706 supplier_loc = Location.search([('usage', '=', 'supplier')], limit=1) 707 if not customer_loc and not supplier_loc: 708 raise UserError(_('Can\'t find any customer or supplier location.')) 709 return customer_loc, supplier_loc 710 711 def _get_route_name(self, route_type): 712 return str(ROUTE_NAMES[route_type]) 713 714 def get_rules_dict(self): 715 """ Define the rules source/destination locations, picking_type and 716 action needed for each warehouse route configuration. 717 """ 718 customer_loc, supplier_loc = self._get_partner_locations() 719 return { 720 warehouse.id: { 721 'one_step': [self.Routing(supplier_loc, warehouse.lot_stock_id, warehouse.in_type_id, 'pull')], 722 'two_steps': [ 723 self.Routing(supplier_loc, warehouse.wh_input_stock_loc_id, warehouse.in_type_id, 'pull'), 724 self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')], 725 'three_steps': [ 726 self.Routing(supplier_loc, warehouse.wh_input_stock_loc_id, warehouse.in_type_id, 'pull'), 727 self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id, 'pull_push'), 728 self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')], 729 'crossdock': [ 730 self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id, 'pull'), 731 self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], 732 'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id, 'pull')], 733 'pick_ship': [ 734 self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id, 'pull'), 735 self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], 736 'pick_pack_ship': [ 737 self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id, 'pull'), 738 self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id, 'pull'), 739 self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')], 740 'company_id': warehouse.company_id.id, 741 } for warehouse in self 742 } 743 744 def _get_receive_rules_dict(self): 745 """ Return receive route rules without initial pull rule in order to update warehouse routes. 746 747 This function has the same receive route rules as get_rules_dict without an initial pull rule. 748 This is expected to be used by modules that extend stock and add actions that can trigger receive 749 'make_to_order' rules (i.e. we don't expect the receive route to be able to pull on its own anymore). 750 This is also expected to be used in conjuction with _get_receive_routes_values() 751 """ 752 return { 753 'one_step': [], 754 'two_steps': [self.Routing(self.wh_input_stock_loc_id, self.lot_stock_id, self.int_type_id, 'pull_push')], 755 'three_steps': [ 756 self.Routing(self.wh_input_stock_loc_id, self.wh_qc_stock_loc_id, self.int_type_id, 'pull_push'), 757 self.Routing(self.wh_qc_stock_loc_id, self.lot_stock_id, self.int_type_id, 'pull_push')], 758 } 759 760 def _get_inter_warehouse_route_values(self, supplier_warehouse): 761 return { 762 'name': _('%(warehouse)s: Supply Product from %(supplier)s', warehouse=self.name, supplier=supplier_warehouse.name), 763 'warehouse_selectable': True, 764 'product_selectable': True, 765 'product_categ_selectable': True, 766 'supplied_wh_id': self.id, 767 'supplier_wh_id': supplier_warehouse.id, 768 'company_id': self.company_id.id, 769 } 770 771 # Pull / Push tools 772 # ------------------------------------------------------------ 773 774 def _get_rule_values(self, route_values, values=None, name_suffix=''): 775 first_rule = True 776 rules_list = [] 777 for routing in route_values: 778 route_rule_values = { 779 'name': self._format_rulename(routing.from_loc, routing.dest_loc, name_suffix), 780 'location_src_id': routing.from_loc.id, 781 'location_id': routing.dest_loc.id, 782 'action': routing.action, 783 'auto': 'manual', 784 'picking_type_id': routing.picking_type.id, 785 'procure_method': first_rule and 'make_to_stock' or 'make_to_order', 786 'warehouse_id': self.id, 787 'company_id': self.company_id.id, 788 } 789 route_rule_values.update(values or {}) 790 rules_list.append(route_rule_values) 791 first_rule = False 792 if values and values.get('propagate_cancel') and rules_list: 793 # In case of rules chain with cancel propagation set, we need to stop 794 # the cancellation for the last step in order to avoid cancelling 795 # any other move after the chain. 796 # Example: In the following flow: 797 # Input -> Quality check -> Stock -> Customer 798 # We want that cancelling I->GC cancel QC -> S but not S -> C 799 # which means: 800 # Input -> Quality check should have propagate_cancel = True 801 # Quality check -> Stock should have propagate_cancel = False 802 rules_list[-1]['propagate_cancel'] = False 803 return rules_list 804 805 def _get_supply_pull_rules_values(self, route_values, values=None): 806 pull_values = {} 807 pull_values.update(values) 808 pull_values.update({'active': True}) 809 rules_list = self._get_rule_values(route_values, values=pull_values) 810 for pull_rules in rules_list: 811 pull_rules['procure_method'] = self.lot_stock_id.id != pull_rules['location_src_id'] and 'make_to_order' or 'make_to_stock' # first part of the resuply route is MTS 812 return rules_list 813 814 def _update_reception_delivery_resupply(self, reception_new, delivery_new): 815 """ Check if we need to change something to resupply warehouses and associated MTO rules """ 816 for warehouse in self: 817 input_loc, output_loc = warehouse._get_input_output_locations(reception_new, delivery_new) 818 if reception_new and warehouse.reception_steps != reception_new and (warehouse.reception_steps == 'one_step' or reception_new == 'one_step'): 819 warehouse._check_reception_resupply(input_loc) 820 if delivery_new and warehouse.delivery_steps != delivery_new and (warehouse.delivery_steps == 'ship_only' or delivery_new == 'ship_only'): 821 change_to_multiple = warehouse.delivery_steps == 'ship_only' 822 warehouse._check_delivery_resupply(output_loc, change_to_multiple) 823 824 def _check_delivery_resupply(self, new_location, change_to_multiple): 825 """ Check if the resupply routes from this warehouse follow the changes of number of delivery steps 826 Check routes being delivery bu this warehouse and change the rule going to transit location """ 827 Rule = self.env["stock.rule"] 828 routes = self.env['stock.location.route'].search([('supplier_wh_id', '=', self.id)]) 829 rules = Rule.search(['&', '&', ('route_id', 'in', routes.ids), ('action', '!=', 'push'), ('location_id.usage', '=', 'transit')]) 830 rules.write({ 831 'location_src_id': new_location.id, 832 'procure_method': change_to_multiple and "make_to_order" or "make_to_stock"}) 833 if not change_to_multiple: 834 # If single delivery we should create the necessary MTO rules for the resupply 835 routings = [self.Routing(self.lot_stock_id, location, self.out_type_id, 'pull') for location in rules.mapped('location_id')] 836 mto_vals = self._get_global_route_rules_values().get('mto_pull_id') 837 values = mto_vals['create_values'] 838 mto_rule_vals = self._get_rule_values(routings, values, name_suffix='MTO') 839 840 for mto_rule_val in mto_rule_vals: 841 Rule.create(mto_rule_val) 842 else: 843 # We need to delete all the MTO stock rules, otherwise they risk to be used in the system 844 Rule.search([ 845 '&', ('route_id', '=', self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id), 846 ('location_id.usage', '=', 'transit'), 847 ('action', '!=', 'push'), 848 ('location_src_id', '=', self.lot_stock_id.id)]).write({'active': False}) 849 850 def _check_reception_resupply(self, new_location): 851 """ Check routes being delivered by the warehouses (resupply routes) and 852 change their rule coming from the transit location """ 853 routes = self.env['stock.location.route'].search([('supplied_wh_id', 'in', self.ids)]) 854 self.env['stock.rule'].search([ 855 '&', 856 ('route_id', 'in', routes.ids), 857 '&', 858 ('action', '!=', 'push'), 859 ('location_src_id.usage', '=', 'transit') 860 ]).write({'location_id': new_location.id}) 861 862 def _update_name_and_code(self, new_name=False, new_code=False): 863 if new_code: 864 self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code}) 865 if new_name: 866 # TDE FIXME: replacing the route name ? not better to re-generate the route naming ? 867 for warehouse in self: 868 routes = warehouse.route_ids 869 for route in routes: 870 route.write({'name': route.name.replace(warehouse.name, new_name, 1)}) 871 for pull in route.rule_ids: 872 pull.write({'name': pull.name.replace(warehouse.name, new_name, 1)}) 873 if warehouse.mto_pull_id: 874 warehouse.mto_pull_id.write({'name': warehouse.mto_pull_id.name.replace(warehouse.name, new_name, 1)}) 875 for warehouse in self: 876 sequence_data = warehouse._get_sequence_values() 877 # `ir.sequence` write access is limited to system user 878 if self.user_has_groups('stock.group_stock_manager'): 879 warehouse = warehouse.sudo() 880 warehouse.in_type_id.sequence_id.write(sequence_data['in_type_id']) 881 warehouse.out_type_id.sequence_id.write(sequence_data['out_type_id']) 882 warehouse.pack_type_id.sequence_id.write(sequence_data['pack_type_id']) 883 warehouse.pick_type_id.sequence_id.write(sequence_data['pick_type_id']) 884 warehouse.int_type_id.sequence_id.write(sequence_data['int_type_id']) 885 886 def _update_location_reception(self, new_reception_step): 887 self.mapped('wh_qc_stock_loc_id').write({'active': new_reception_step == 'three_steps'}) 888 self.mapped('wh_input_stock_loc_id').write({'active': new_reception_step != 'one_step'}) 889 890 def _update_location_delivery(self, new_delivery_step): 891 self.mapped('wh_pack_stock_loc_id').write({'active': new_delivery_step == 'pick_pack_ship'}) 892 self.mapped('wh_output_stock_loc_id').write({'active': new_delivery_step != 'ship_only'}) 893 894 # Misc 895 # ------------------------------------------------------------ 896 897 def _get_picking_type_update_values(self): 898 """ Return values in order to update the existing picking type when the 899 warehouse's delivery_steps or reception_steps are modify. 900 """ 901 input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps) 902 return { 903 'in_type_id': { 904 'default_location_dest_id': input_loc.id, 905 'barcode': self.code.replace(" ", "").upper() + "-RECEIPTS", 906 }, 907 'out_type_id': { 908 'default_location_src_id': output_loc.id, 909 'barcode': self.code.replace(" ", "").upper() + "-DELIVERY", 910 }, 911 'pick_type_id': { 912 'active': self.delivery_steps != 'ship_only' and self.active, 913 'default_location_dest_id': output_loc.id if self.delivery_steps == 'pick_ship' else self.wh_pack_stock_loc_id.id, 914 'barcode': self.code.replace(" ", "").upper() + "-PICK", 915 }, 916 'pack_type_id': { 917 'active': self.delivery_steps == 'pick_pack_ship' and self.active, 918 'barcode': self.code.replace(" ", "").upper() + "-PACK", 919 }, 920 'int_type_id': { 921 'barcode': self.code.replace(" ", "").upper() + "-INTERNAL", 922 }, 923 } 924 925 def _get_picking_type_create_values(self, max_sequence): 926 """ When a warehouse is created this method return the values needed in 927 order to create the new picking types for this warehouse. Every picking 928 type are created at the same time than the warehouse howver they are 929 activated or archived depending the delivery_steps or reception_steps. 930 """ 931 input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps) 932 return { 933 'in_type_id': { 934 'name': _('Receipts'), 935 'code': 'incoming', 936 'use_create_lots': True, 937 'use_existing_lots': False, 938 'default_location_src_id': False, 939 'sequence': max_sequence + 1, 940 'show_reserved': False, 941 'show_operations': False, 942 'sequence_code': 'IN', 943 'company_id': self.company_id.id, 944 }, 'out_type_id': { 945 'name': _('Delivery Orders'), 946 'code': 'outgoing', 947 'use_create_lots': False, 948 'use_existing_lots': True, 949 'default_location_dest_id': False, 950 'sequence': max_sequence + 5, 951 'sequence_code': 'OUT', 952 'company_id': self.company_id.id, 953 }, 'pack_type_id': { 954 'name': _('Pack'), 955 'code': 'internal', 956 'use_create_lots': False, 957 'use_existing_lots': True, 958 'default_location_src_id': self.wh_pack_stock_loc_id.id, 959 'default_location_dest_id': output_loc.id, 960 'sequence': max_sequence + 4, 961 'sequence_code': 'PACK', 962 'company_id': self.company_id.id, 963 }, 'pick_type_id': { 964 'name': _('Pick'), 965 'code': 'internal', 966 'use_create_lots': False, 967 'use_existing_lots': True, 968 'default_location_src_id': self.lot_stock_id.id, 969 'sequence': max_sequence + 3, 970 'sequence_code': 'PICK', 971 'company_id': self.company_id.id, 972 }, 'int_type_id': { 973 'name': _('Internal Transfers'), 974 'code': 'internal', 975 'use_create_lots': False, 976 'use_existing_lots': True, 977 'default_location_src_id': self.lot_stock_id.id, 978 'default_location_dest_id': self.lot_stock_id.id, 979 'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'), 980 'sequence': max_sequence + 2, 981 'sequence_code': 'INT', 982 'company_id': self.company_id.id, 983 }, 984 }, max_sequence + 6 985 986 def _get_sequence_values(self): 987 """ Each picking type is created with a sequence. This method returns 988 the sequence values associated to each picking type. 989 """ 990 return { 991 'in_type_id': { 992 'name': self.name + ' ' + _('Sequence in'), 993 'prefix': self.code + '/IN/', 'padding': 5, 994 'company_id': self.company_id.id, 995 }, 996 'out_type_id': { 997 'name': self.name + ' ' + _('Sequence out'), 998 'prefix': self.code + '/OUT/', 'padding': 5, 999 'company_id': self.company_id.id, 1000 }, 1001 'pack_type_id': { 1002 'name': self.name + ' ' + _('Sequence packing'), 1003 'prefix': self.code + '/PACK/', 'padding': 5, 1004 'company_id': self.company_id.id, 1005 }, 1006 'pick_type_id': { 1007 'name': self.name + ' ' + _('Sequence picking'), 1008 'prefix': self.code + '/PICK/', 'padding': 5, 1009 'company_id': self.company_id.id, 1010 }, 1011 'int_type_id': { 1012 'name': self.name + ' ' + _('Sequence internal'), 1013 'prefix': self.code + '/INT/', 'padding': 5, 1014 'company_id': self.company_id.id, 1015 }, 1016 } 1017 1018 def _format_rulename(self, from_loc, dest_loc, suffix): 1019 rulename = '%s: %s' % (self.code, from_loc.name) 1020 if dest_loc: 1021 rulename += ' → %s' % (dest_loc.name) 1022 if suffix: 1023 rulename += ' (' + suffix + ')' 1024 return rulename 1025 1026 def _format_routename(self, name=None, route_type=None): 1027 if route_type: 1028 name = self._get_route_name(route_type) 1029 return '%s: %s' % (self.name, name) 1030 1031 @api.returns('self') 1032 def _get_all_routes(self): 1033 routes = self.mapped('route_ids') | self.mapped('mto_pull_id').mapped('route_id') 1034 routes |= self.env["stock.location.route"].search([('supplied_wh_id', 'in', self.ids)]) 1035 return routes 1036 1037 def action_view_all_routes(self): 1038 routes = self._get_all_routes() 1039 return { 1040 'name': _('Warehouse\'s Routes'), 1041 'domain': [('id', 'in', routes.ids)], 1042 'res_model': 'stock.location.route', 1043 'type': 'ir.actions.act_window', 1044 'view_id': False, 1045 'view_mode': 'tree,form', 1046 'limit': 20, 1047 'context': dict(self._context, default_warehouse_selectable=True, default_warehouse_ids=self.ids) 1048 } 1049