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