diff --git a/addons/mrp/stock.py b/addons/mrp/stock.py index f1c3ba524f1..be17a61b6b1 100644 --- a/addons/mrp/stock.py +++ b/addons/mrp/stock.py @@ -3,6 +3,7 @@ import time +from openerp import api from openerp.osv import fields from openerp.osv import osv from openerp.tools.translate import _ @@ -246,7 +247,7 @@ class stock_warehouse(osv.osv): raise UserError(_('Can\'t find any generic Manufacture route.')) return { - 'name': self._format_routename(cr, uid, warehouse, _(' Manufacture'), context=context), + 'name': warehouse._format_routename(_(' Manufacture')), 'location_id': warehouse.lot_stock_id.id, 'route_id': manufacture_route_id, 'action': 'manufacture', @@ -283,12 +284,11 @@ class stock_warehouse(osv.osv): pull_obj.unlink(cr, uid, warehouse.manufacture_pull_id.id, context=context) return super(stock_warehouse, self).write(cr, uid, ids, vals, context=context) - def get_all_routes_for_wh(self, cr, uid, ids, context=None): - all_routes = super(stock_warehouse, self).get_all_routes_for_wh(cr, uid, ids, context=context) - warehouse = self.browse(cr, uid, ids[0], context=context) - if warehouse.manufacture_to_resupply and warehouse.manufacture_pull_id and warehouse.manufacture_pull_id.route_id: - all_routes += [warehouse.manufacture_pull_id.route_id.id] - return all_routes + @api.multi + def _get_all_routes(self): + routes = super(stock_warehouse, self).get_all_routes_for_wh() + routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id') + return routes def _handle_renaming(self, cr, uid, ids, name, code, context=None): res = super(stock_warehouse, self)._handle_renaming(cr, uid, ids, name, code, context=context) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 2d8d63a490c..743a1684557 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -526,7 +526,7 @@ class PosOrder(models.Model): destination_id = order.partner_id.property_stock_customer.id else: if (not picking_type) or (not picking_type.default_location_dest_id): - customerloc, supplierloc = StockWarehouse._get_partner_locations([]) + customerloc, supplierloc = StockWarehouse._get_partner_locations() destination_id = customerloc.id else: destination_id = picking_type.default_location_dest_id.id diff --git a/addons/product/product.py b/addons/product/product.py index f04c53b2cd5..eebfd38a550 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -104,6 +104,7 @@ class product_uom(osv.osv): ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!') ] + @api.cr_uid def _compute_qty(self, cr, uid, from_uom_id, qty, to_uom_id=False, round=True, rounding_method='UP'): if not from_uom_id or not qty or not to_uom_id: return qty diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index 36eb8c45b45..168f4cdeef4 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -80,7 +80,7 @@ class stock_warehouse(osv.osv): raise UserError(_('Can\'t find any generic Buy route.')) return { - 'name': self._format_routename(cr, uid, warehouse, _(' Buy'), context=context), + 'name': warehouse._format_routename(_(' Buy')), 'location_id': warehouse.in_type_id.default_location_dest_id.id, 'route_id': buy_route_id, 'action': 'buy', @@ -117,12 +117,11 @@ class stock_warehouse(osv.osv): buy_pull_id = pull_obj.unlink(cr, uid, warehouse.buy_pull_id.id, context=context) return super(stock_warehouse, self).write(cr, uid, ids, vals, context=None) - def get_all_routes_for_wh(self, cr, uid, ids, context=None): - all_routes = super(stock_warehouse, self).get_all_routes_for_wh(cr, uid, ids, context=context) - warehouse = self.browse(cr, uid, ids[0], context=context) - if warehouse.buy_to_resupply and warehouse.buy_pull_id and warehouse.buy_pull_id.route_id: - all_routes += [warehouse.buy_pull_id.route_id.id] - return all_routes + @api.multi + def _get_all_routes(self): + routes = super(stock_warehouse, self).get_all_routes_for_wh() + routes |= self.filtered(lambda self: self.buy_to_resupply and self.buy_pull_id and self.buy_pull_id.route_id).mapped('buy_pull_id').mapped('route_id') + return routes def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None): res = super(stock_warehouse, self)._get_all_products_to_resupply(cr, uid, warehouse, context=context) @@ -143,9 +142,9 @@ class stock_warehouse(osv.osv): pull_obj.write(cr, uid, warehouse.buy_pull_id.id, {'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)}, context=context) return res - def change_route(self, cr, uid, ids, new_reception_step=False, new_delivery_step=False, context=None): - res = super(stock_warehouse, self).change_route(cr, uid, ids, new_reception_step=new_reception_step, new_delivery_step=new_delivery_step, context=context) - warehouse = self.browse(cr, uid, ids[0], context=context) - if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id: - self.pool.get('procurement.rule').write(cr, uid, warehouse.buy_pull_id.id, {'location_id': warehouse.in_type_id.default_location_dest_id.id}, context=context) + def change_route(self, cr, uid, ids, context=None): + res = super(stock_warehouse, self).change_route(cr, uid, ids, context=context) + for warehouse in self.browse(cr, uid, ids[0], context=context): + if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id: + self.pool.get('procurement.rule').write(cr, uid, warehouse.buy_pull_id.id, {'location_id': warehouse.in_type_id.default_location_dest_id.id}, context=context) return res diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index b4e5bceed22..979acce6a25 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -228,13 +228,13 @@ class SaleOrderLine(models.Model): if wh_mto_route and wh_mto_route <= product_routes: is_available = True else: - mto_route_id = False + mto_route = False try: - mto_route_id = self.env['stock.warehouse']._get_mto_route() + mto_route = self.env['stock.warehouse']._get_mto_route() except UserError: # if route MTO not found in ir_model_data, we treat the product as in MTS pass - if mto_route_id and mto_route_id in product_routes.ids: + if mto_route and mto_route in product_routes: is_available = True # Check Drop-Shipping diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 763260046fc..e3f6f1407e5 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -1,770 +1,692 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import namedtuple from datetime import datetime from dateutil import relativedelta -from openerp.osv import fields, osv -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT -from openerp.tools.translate import _ -from openerp import SUPERUSER_ID -import openerp.addons.decimal_precision as dp -from openerp.exceptions import UserError +from odoo import api, fields, models, _ +from odoo.addons import decimal_precision as dp +from odoo.exceptions import UserError, ValidationError +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT -class stock_warehouse(osv.osv): +class Warehouse(models.Model): _name = "stock.warehouse" _description = "Warehouse" + # namedtuple used in helper methods generating values for routes + Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type']) - _columns = { - 'name': fields.char('Warehouse Name', required=True, select=True), - 'active': fields.boolean('Active'), - 'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, select=True, help='The company is automatically set from your user preferences.'), - 'partner_id': fields.many2one('res.partner', 'Address'), - 'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]), - 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', domain=[('usage', '=', 'internal')], required=True), - 'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"), - 'route_ids': fields.many2many('stock.location.route', 'stock_route_warehouse', 'warehouse_id', 'route_id', 'Routes', domain="[('warehouse_selectable', '=', True)]", help='Defaults routes through the warehouse'), - 'reception_steps': fields.selection([ - ('one_step', 'Receive goods directly in stock (1 step)'), - ('two_steps', 'Unload in input location then go to stock (2 steps)'), - ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments', - help="Default incoming route to follow", required=True), - 'delivery_steps': fields.selection([ - ('ship_only', 'Ship directly from stock (Ship only)'), - ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'), - ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], 'Outgoing Shippings', - help="Default outgoing route to follow", required=True), - 'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'), - 'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'), - 'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'), - 'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'), - 'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'), - 'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'), - 'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'), - 'out_type_id': fields.many2one('stock.picking.type', 'Out Type'), - 'in_type_id': fields.many2one('stock.picking.type', 'In Type'), - 'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'), - 'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'), - 'reception_route_id': fields.many2one('stock.location.route', 'Receipt Route'), - 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'), - 'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'), - 'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes', - help="Routes will be created for these resupply warehouses and you can select them on products and product categories"), - 'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse', help="Goods will always be resupplied from this warehouse"), - } + name = fields.Char('Warehouse Name', index=True, required=True) + active = fields.Boolean('Active', default=True) + company_id = fields.Many2one( + 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.inventory'), + index=True, readonly=True, required=True, + help='The company is automatically set from your user preferences.') + partner_id = fields.Many2one('res.partner', 'Address') + view_location_id = fields.Many2one('stock.location', 'View Location', domain=[('usage', '=', 'view')], required=True) + lot_stock_id = fields.Many2one('stock.location', 'Location Stock', domain=[('usage', '=', 'internal')], required=True) + code = fields.Char('Short Name', required=True, size=5, help="Short name used to identify your warehouse") + route_ids = fields.Many2many( + 'stock.location.route', 'stock_route_warehouse', 'warehouse_id', 'route_id', + 'Routes', domain="[('warehouse_selectable', '=', True)]", + help='Defaults routes through the warehouse') + reception_steps = fields.Selection([ + ('one_step', 'Receive goods directly in stock (1 step)'), + ('two_steps', 'Unload in input location then go to stock (2 steps)'), + ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], + 'Incoming Shipments', default='one_step', required=True, + help="Default incoming route to follow") + delivery_steps = fields.Selection([ + ('ship_only', 'Ship directly from stock (Ship only)'), + ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'), + ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], + 'Outgoing Shippings', default='ship_only', required=True, + help="Default outgoing route to follow") + wh_input_stock_loc_id = fields.Many2one('stock.location', 'Input Location') + wh_qc_stock_loc_id = fields.Many2one('stock.location', 'Quality Control Location') + wh_output_stock_loc_id = fields.Many2one('stock.location', 'Output Location') + wh_pack_stock_loc_id = fields.Many2one('stock.location', 'Packing Location') + mto_pull_id = fields.Many2one('procurement.rule', 'MTO rule') + pick_type_id = fields.Many2one('stock.picking.type', 'Pick Type') + pack_type_id = fields.Many2one('stock.picking.type', 'Pack Type') + out_type_id = fields.Many2one('stock.picking.type', 'Out Type') + in_type_id = fields.Many2one('stock.picking.type', 'In Type') + int_type_id = fields.Many2one('stock.picking.type', 'Internal Type') + crossdock_route_id = fields.Many2one('stock.location.route', 'Crossdock Route') + reception_route_id = fields.Many2one('stock.location.route', 'Receipt Route') + delivery_route_id = fields.Many2one('stock.location.route', 'Delivery Route') + resupply_wh_ids = fields.Many2many( + 'stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', + 'Resupply Warehouses') + resupply_route_ids = fields.One2many( + 'stock.location.route', 'supplied_wh_id', 'Resupply Routes', + help="Routes will be created for these resupply warehouses and you can select them on products and product categories") + default_resupply_wh_id = fields.Many2one( + 'stock.warehouse', 'Default Resupply Warehouse', + help="Goods will always be resupplied from this warehouse") - def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None): - resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))]) - if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id - resupply_wh_ids.add(default_resupply_wh_id) - resupply_wh_ids = list(resupply_wh_ids) - return {'value': {'resupply_wh_ids': resupply_wh_ids}} - - def _get_external_transit_location(self, cr, uid, ids, context=None): - ''' returns browse record of inter company transit location, if found''' - warehouse = self.browse(cr, uid, ids[0], context=context) - data_obj = self.pool.get('ir.model.data') - location_obj = self.pool.get('stock.location') - try: - inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1] - except: - return False - return location_obj.browse(cr, uid, inter_wh_loc, context=context) - - def _get_inter_wh_route(self, cr, uid, ids, wh, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - return { - 'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name), - 'warehouse_selectable': False, - 'product_selectable': True, - 'product_categ_selectable': True, - 'supplied_wh_id': warehouse.id, - 'supplier_wh_id': wh.id, - } - - def _create_resupply_routes(self, cr, uid, ids, supplier_warehouses, default_resupply_wh, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - route_obj = self.pool.get('stock.location.route') - pull_obj = self.pool.get('procurement.rule') - #create route selectable on the product to resupply the warehouse from another one - external_transit_location = warehouse._get_external_transit_location() - internal_transit_location = warehouse.company_id.internal_transit_location_id - input_loc = warehouse.wh_input_stock_loc_id - if warehouse.reception_steps == 'one_step': - input_loc = warehouse.lot_stock_id - for wh in supplier_warehouses: - transit_location = wh.company_id.id == warehouse.company_id.id and internal_transit_location or external_transit_location - if transit_location: - output_loc = wh.wh_output_stock_loc_id - if wh.delivery_steps == 'ship_only': - output_loc = wh.lot_stock_id - # Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists) - mto_pull_vals = wh._get_mto_pull_rule([(output_loc, transit_location, wh.out_type_id.id)])[0] - pull_obj.create(cr, uid, mto_pull_vals, context=context) - inter_wh_route_vals = warehouse._get_inter_wh_route(wh) - inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context) - values = [(output_loc, transit_location, wh.out_type_id.id, wh), (transit_location, input_loc, warehouse.in_type_id.id, warehouse)] - pull_rules_list = self._get_supply_pull_rules(cr, uid, [wh.id], values, inter_wh_route_id, context=context) - for pull_rule in pull_rules_list: - pull_obj.create(cr, uid, vals=pull_rule, context=context) - #if the warehouse is also set as default resupply method, assign this route automatically to the warehouse - if default_resupply_wh and default_resupply_wh.id == wh.id: - self.write(cr, uid, [warehouse.id, wh.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context) - - _defaults = { - 'active': True, - 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c), - 'reception_steps': 'one_step', - 'delivery_steps': 'ship_only', - } _sql_constraints = [ ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'), ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'), ] - def _get_partner_locations(self, cr, uid, ids, context=None): - ''' returns a tuple made of the browse record of customer location and the browse record of supplier location''' - data_obj = self.pool.get('ir.model.data') - location_obj = self.pool.get('stock.location') - try: - customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1] - supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1] - except: - customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context) - customer_loc = customer_loc and customer_loc[0] or False - supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context) - supplier_loc = supplier_loc and supplier_loc[0] or False - if not (customer_loc and supplier_loc): - raise UserError(_('Can\'t find any customer or supplier location.')) - return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context) + @api.depends('default_resupply_wh_id', 'resupply_wh_ids') + def onchange_resupply_warehouses(self): + # If we are removing the default resupply, we don't have default_resupply_wh_id # TDE note: and we want one + self.resupply_wh_ids |= self.default_resupply_wh_id - def _location_used(self, cr, uid, ids, location_id, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - pull_obj = self.pool['procurement.rule'] - push_obj = self.pool['stock.location.path'] + @api.model + def create(self, vals): + # create view location for warehouse then create all locations + loc_vals = {'name': _(vals.get('code')), 'usage': 'view', + 'location_id': self.env.ref('stock.stock_location_locations').id} + if vals.get('company_id'): + loc_vals['company_id'] = vals.get('company_id') + vals['view_location_id'] = self.env['stock.location'].create(loc_vals).id - domain = ['&', ('route_id', 'not in', [x.id for x in warehouse.route_ids]), - '|', ('location_src_id', '=', location_id), # noqa - ('location_id', '=', location_id) - ] - pulls = pull_obj.search_count(cr, uid, domain, context=context) + def_values = self.default_get(['reception_steps', 'delivery_steps']) + reception_steps = vals.get('reception_steps', def_values['reception_steps']) + delivery_steps = vals.get('delivery_steps', def_values['delivery_steps']) + sub_locations = { + 'lot_stock_id': {'name': _('Stock'), 'active': True, 'usage': 'internal'}, + 'wh_input_stock_loc_id': {'name': _('Input'), 'active': reception_steps != 'one_step', 'usage': 'internal'}, + 'wh_qc_stock_loc_id': {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'usage': 'internal'}, + 'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'}, + 'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'}, + } + for field_name, values in sub_locations.iteritems(): + values['location_id'] = vals['view_location_id'] + if vals.get('company_id'): + values['company_id'] = vals.get('company_id') + vals[field_name] = self.env['stock.location'].with_context(active_test=False).create(values).id - domain = ['&', ('route_id', 'not in', [x.id for x in warehouse.route_ids]), - '|', ('location_from_id', '=', location_id), # noqa - ('location_dest_id', '=', location_id) - ] - pushs = push_obj.search_count(cr, uid, domain, context=context) - if pulls or pushs: - return True - return False + # actually create WH + warehouse = super(Warehouse, self).create(vals) + # create sequences and picking types + new_vals = warehouse.create_sequences_and_picking_types() + warehouse.write(new_vals) # TDE FIXME: use super ? + # create routes and push/procurement rules + route_vals = warehouse.create_routes() + warehouse.write(route_vals) + # update partner data if partner assigned + if vals.get('partner_id'): + self._update_partner_data(vals['partner_id'], vals.get('company_id')) + return warehouse - def switch_location(self, cr, uid, ids, new_reception_step=False, new_delivery_step=False, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - location_obj = self.pool.get('stock.location') + @api.multi + def write(self, vals): + Route = self.env['stock.location.route'] + warehouses = self.with_context(active_test=False) # TDE FIXME: check this - new_reception_step = new_reception_step or warehouse.reception_steps - new_delivery_step = new_delivery_step or warehouse.delivery_steps - if warehouse.reception_steps != new_reception_step: - if not warehouse._location_used(warehouse.wh_input_stock_loc_id.id): - location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context) - if new_reception_step != 'one_step': - location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context) - if new_reception_step == 'three_steps': - location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context) + if vals.get('code') or vals.get('name'): + warehouses._update_name_and_code(vals.get('name'), vals.get('code')) - if warehouse.delivery_steps != new_delivery_step: - if not warehouse._location_used(warehouse.wh_output_stock_loc_id.id): - location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id], {'active': False}, context=context) - if not warehouse._location_used(warehouse.wh_pack_stock_loc_id.id): - location_obj.write(cr, uid, [warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context) - if new_delivery_step != 'ship_only': - location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context) - if new_delivery_step == 'pick_pack_ship': - location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context) - return True + # activate and deactivate location according to reception and delivery option + if vals.get('reception_steps'): + warehouses._update_location_reception(vals['reception_steps']) + if vals.get('delivery_steps'): + warehouses._update_location_delivery(vals['delivery_steps']) + if vals.get('reception_steps') or vals.get('delivery_steps'): + warehouses._update_reception_delivery_resupply(vals.get('reception_steps'), vals.get('delivery_steps')) - def _get_reception_delivery_route(self, cr, uid, ids, route_name, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] + if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): + resupply_whs = self.resolve_2many_commands('resupply_wh_ids', vals['resupply_wh_ids']) + new_resupply_whs = self.browse([wh['id'] for wh in resupply_whs]) + old_resupply_whs = {(warehouse.id, warehouse.resupply_wh_ids) for warehouse in warehouses} + + if 'default_resupply_wh_id' in vals: + if vals.get('default_resupply_wh_id') and any(vals['default_resupply_wh_id'] == warehouse.id for warehouse in warehouses): + raise UserError(_('The default resupply warehouse should be different than the warehouse itself!')) + for warehouse in warehouses.filtered(lambda wh: wh.default_resupply_wh_id): + # remove the existing resupplying route on the warehouse + to_remove_routes = Route.search([('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', warehouse.default_resupply_wh_id.id)]) + for inter_wh_route in to_remove_routes: + warehouse.write({'route_ids': [(3, inter_wh_route.id)]}) + + # If another partner assigned + if vals.get('partner_id'): + warehouses._update_partner_data(vals['partner_id'], vals.get('company_id')) + res = super(Warehouse, self).write(vals) + + # check if we need to delete and recreate route + if vals.get('reception_steps') or vals.get('delivery_steps'): + warehouses._update_routes() + + if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): + for warehouse in warehouses: + to_add = new_resupply_whs - old_resupply_whs[warehouse.id] + to_remove = old_resupply_whs[warehouse.id] - new_resupply_whs + if to_add: + warehouse.create_resupply_routes(to_add, warehouse.default_resupply_wh_id) + if to_remove: + Route.search([('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', 'in', to_remove.ids)]).unlink() + # TDE FIXME: shouldn't we remove procurement rules also ? because this could make them global (not sure) + + return res + + @api.model + def _update_partner_data(self, partner_id, company_id): + if not partner_id: + return + ResCompany = self.env['res.company'] + if company_id: + transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id + else: + transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id + self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc}) + + def create_sequences_and_picking_types(self): + IrSequenceSudo = self.env['ir.sequence'].sudo() + PickingType = self.env['stock.picking.type'] + + input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps) + + # choose the next available color for the picking types of this warehouse + all_used_colors = [res['color'] for res in PickingType.search_read([('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')] + available_colors = [zef for zef in [0, 3, 4, 5, 6, 7, 8, 1, 2] if zef not in all_used_colors] + color = available_colors and available_colors[0] or 0 + + # suit for each warehouse: reception, internal, pick, pack, ship + max_sequence = PickingType.search_read([], ['sequence'], limit=1, order='sequence desc') + max_sequence = max_sequence and max_sequence[0]['sequence'] or 0 + + warehouse_data = {} + sequence_data = self._get_sequence_values() + # tde todo: backport sequence fix + create_data = { + 'in_type_id': { + 'name': _('Receipts'), + 'code': 'incoming', + 'use_create_lots': True, + 'use_existing_lots': False, + 'default_location_src_id': False, + 'sequence': max_sequence + 1, + }, 'out_type_id': { + 'name': _('Delivery Orders'), + 'code': 'outgoing', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_dest_id': False, + 'sequence': max_sequence + 5, + }, 'pack_type_id': { + 'name': _('Pack'), + 'code': 'internal', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_src_id': self.wh_pack_stock_loc_id.id, + 'default_location_dest_id': output_loc.id, + 'sequence': max_sequence + 4, + }, 'pick_type_id': { + 'name': _('Pick'), + 'code': 'internal', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_src_id': self.lot_stock_id.id, + 'sequence': max_sequence + 3, + }, 'int_type_id': { + 'name': _('Internal Transfers'), + 'code': 'internal', + 'use_create_lots': False, + 'use_existing_lots': True, + 'default_location_src_id': self.lot_stock_id.id, + 'default_location_dest_id': self.lot_stock_id.id, + 'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'), + 'sequence': max_sequence + 2, + }, + } + data = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id) + for field_name, values in data.iteritems(): + data[field_name].update(create_data[field_name]) + + for picking_type, values in data.iteritems(): + sequence = IrSequenceSudo.create(sequence_data[picking_type]) + values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id) + warehouse_data[picking_type] = PickingType.create(values).id + PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']}) + PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']}) + return warehouse_data + + @api.multi + def create_routes(self): + self.ensure_one() + routes_data = self.get_routes_dict() + + reception_route = self._create_or_update_reception_route(routes_data) + delivery_route = self._create_or_update_delivery_route(routes_data) + mto_pull = self._create_or_update_mto_pull(routes_data) + crossdock_route = self._create_or_update_crossdock_route(routes_data) + + # create route selectable on the product to resupply the warehouse from another one + self.create_resupply_routes(self.resupply_wh_ids, self.default_resupply_wh_id) + + # return routes and mto procurement rule to store on the warehouse return { - 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), + 'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route], + 'mto_pull_id': mto_pull.id, + 'reception_route_id': reception_route.id, + 'delivery_route_id': delivery_route.id, + 'crossdock_route_id': crossdock_route.id, + } + + def _create_or_update_reception_route(self, routes_data): + routes_data = routes_data or self.get_routes_dict() + for warehouse in self: + if warehouse.reception_route_id: + reception_route = warehouse.reception_route_id + reception_route.write({'name': warehouse._format_routename(route_type=warehouse.reception_steps)}) + reception_route.pull_ids.unlink() + reception_route.push_ids.unlink() + else: + reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps)) + # push / procurement (pull) rules for reception + routings = routes_data[warehouse.id][warehouse.reception_steps] + push_rules_list, pull_rules_list = warehouse._get_push_pull_rules_values( + routings, values={'active': True, 'route_id': reception_route.id}, + push_values=None, pull_values={'procure_method': 'make_to_order'}) + for push_vals in push_rules_list: + self.env['stock.location.path'].create(push_vals) + for pull_vals in pull_rules_list: + self.env['procurement.rule'].create(pull_vals) + return reception_route + + def _create_or_update_delivery_route(self, routes_data): + """ Delivery (MTS) route """ + routes_data = routes_data or self.get_routes_dict() + for warehouse in self: + if warehouse.delivery_route_id: + delivery_route = warehouse.delivery_route_id + delivery_route.write({'name': warehouse._format_routename(route_type=warehouse.delivery_steps)}) + delivery_route.pull_ids.unlink() + else: + delivery_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.delivery_steps)) + # procurement (pull) rules for delivery + routings = routes_data[warehouse.id][warehouse.delivery_steps] + dummy, pull_rules_list = warehouse._get_push_pull_rules_values( + routings, values={'active': True, 'route_id': delivery_route.id}) + for pull_vals in pull_rules_list: + self.env['procurement.rule'].create(pull_vals) + return delivery_route + + def _create_or_update_mto_pull(self, routes_data): + """ Create MTO procurement rule and link it to the generic MTO route """ + routes_data = routes_data or self.get_routes_dict() + for warehouse in self: + routings = routes_data[warehouse.id][warehouse.delivery_steps] + if warehouse.mto_pull_id: + mto_pull = warehouse.mto_pull_id + mto_pull.write(warehouse._get_mto_pull_rules_values(routings)[0]) + else: + mto_pull = self.env['procurement.rule'].create(warehouse._get_mto_pull_rules_values(routings)[0]) + return mto_pull + + def _create_or_update_crossdock_route(self, routes_data): + """ Create or update the cross dock operations route, that can be set on + products and product categories """ + routes_data = routes_data or self.get_routes_dict() + for warehouse in self: + if warehouse.crossdock_route_id: + crossdock_route = warehouse.crossdock_route_id + crossdock_route.write({'active': warehouse.reception_steps != 'one_step' and warehouse.delivery_steps != 'ship_only'}) + else: + crossdock_route = self.env['stock.location.route'].create(warehouse._get_crossdock_route_values()) + # note: fixed cross-dock is logically mto + routings = routes_data[warehouse.id]['crossdock'] + dummy, pull_rules_list = warehouse._get_push_pull_rules_values( + routings, + values={'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', 'route_id': crossdock_route.id}, + push_values=None, pull_values={'procure_method': 'make_to_order'}) + for pull_vals in pull_rules_list: + self.env['procurement.rule'].create(pull_vals) + return crossdock_route + + def create_resupply_routes(self, supplier_warehouses, default_resupply_wh): + Route = self.env['stock.location.route'] + Pull = self.env['procurement.rule'] + + input_location, output_location = self._get_input_output_locations(self.reception_steps, self.delivery_steps) + internal_transit_location, external_transit_location = self._get_transit_locations() + + for supplier_wh in supplier_warehouses: + transit_location = internal_transit_location if supplier_wh.company_id == self.company_id else external_transit_location + if not transit_location: + continue + output_location = supplier_wh.lot_stock_id if supplier_wh.delivery_steps == 'ship_only' else supplier_wh.wh_output_stock_loc_id + # Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists) + if supplier_wh.delivery_steps == 'ship_only': + Pull.create(supplier_wh._get_mto_pull_rules_values([ + self.Routing(output_location, transit_location, supplier_wh.out_type_id)])[0]) + + inter_wh_route = Route.create(self._get_inter_warehouse_route_values(supplier_wh)) + + pull_rules_list = supplier_wh._get_supply_pull_rules_values( + [self.Routing(output_location, transit_location, supplier_wh.out_type_id)], + values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id}) + pull_rules_list += self._get_supply_pull_rules_values( + [self.Routing(transit_location, input_location, self.in_type_id)], + values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id}) + for pull_rule_vals in pull_rules_list: + Pull.create(pull_rule_vals) + + # if the warehouse is also set as default resupply method, assign this route automatically to the warehouse + if supplier_wh == default_resupply_wh: + (self | supplier_wh).write({'route_ids': [(4, inter_wh_route.id)]}) + + # Routing tools + # ------------------------------------------------------------ + + def _get_input_output_locations(self, reception_steps, delivery_steps): + return (self.lot_stock_id if reception_steps == 'one_step' else self.wh_input_stock_loc_id, + self.lot_stock_id if delivery_steps == 'ship_only' else self.wh_output_stock_loc_id) + + def _get_transit_locations(self): + 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'] + + @api.model + def _get_partner_locations(self): + ''' returns a tuple made of the browse record of customer location and the browse record of supplier location''' + Location = self.env['stock.location'] + customer_loc = self.env.ref('stock.stock_location_customers', raise_if_not_found=False) + supplier_loc = self.env.ref('stock.stock_location_suppliers', raise_if_not_found=False) + if not customer_loc: + customer_loc = Location.search([('usage', '=', 'customer')], limit=1) + if not supplier_loc: + supplier_loc = Location.search([('usage', '=', 'supplier')], limit=1) + if not customer_loc and not supplier_loc: + raise UserError(_('Can\'t find any customer or supplier location.')) + return customer_loc, supplier_loc + + def _get_route_name(self, route_type): + names = {'one_step': _('Receipt in 1 step'), 'two_steps': _('Receipt in 2 steps'), + 'three_steps': _('Receipt in 3 steps'), 'crossdock': _('Cross-Dock'), + 'ship_only': _('Ship Only'), 'pick_ship': _('Pick + Ship'), + 'pick_pack_ship': _('Pick + Pack + Ship')} + return names[route_type] + + def get_routes_dict(self): + # TDE todo: rename me (was get_routes_dict) + customer_loc, supplier_loc = self._get_partner_locations() + return dict((warehouse.id, { + 'one_step': [], + 'two_steps': [self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id)], + 'three_steps': [ + self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id), + self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id)], + 'crossdock': [ + self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id), + self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)], + 'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id)], + 'pick_ship': [ + self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id), + self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)], + 'pick_pack_ship': [ + self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id), + self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id), + self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)], + }) for warehouse in self) + + @api.multi + def _get_reception_delivery_route_values(self, route_type): + return { + 'name': self._format_routename(route_type=route_type), 'product_categ_selectable': True, 'product_selectable': False, 'sequence': 10, } - def _get_supply_pull_rules(self, cr, uid, ids, values, new_route_id, context=None): - supply_warehouse = ids[0] - pull_rules_list = [] - for from_loc, dest_loc, pick_type_id, warehouse in values: - pull_rules_list.append({ - 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context), - 'location_src_id': from_loc.id, - 'location_id': dest_loc.id, - 'route_id': new_route_id, - 'action': 'move', - 'picking_type_id': pick_type_id, - 'procure_method': warehouse.lot_stock_id.id != from_loc.id and 'make_to_order' or 'make_to_stock', # first part of the resuply route is MTS - 'warehouse_id': warehouse.id, - 'propagate_warehouse_id': supply_warehouse, - }) - return pull_rules_list - - def _get_push_pull_rules(self, cr, uid, ids, active, values, new_route_id, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - first_rule = True - push_rules_list = [] - pull_rules_list = [] - for from_loc, dest_loc, pick_type_id in values: - push_rules_list.append({ - 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context), - 'location_from_id': from_loc.id, - 'location_dest_id': dest_loc.id, - 'route_id': new_route_id, - 'auto': 'manual', - 'picking_type_id': pick_type_id, - 'active': active, - 'warehouse_id': warehouse.id, - }) - pull_rules_list.append({ - 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context), - 'location_src_id': from_loc.id, - 'location_id': dest_loc.id, - 'route_id': new_route_id, - 'action': 'move', - 'picking_type_id': pick_type_id, - 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order', - 'active': active, - 'warehouse_id': warehouse.id, - }) - first_rule = False - return push_rules_list, pull_rules_list - - def _get_mto_route(self, cr, uid, context=None): - route_obj = self.pool.get('stock.location.route') - data_obj = self.pool.get('ir.model.data') - try: - mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1] - except: - mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('Make To Order'))], context=context) - mto_route_id = mto_route_id and mto_route_id[0] or False - if not mto_route_id: + @api.model + @api.returns('stock.location.route', lambda value: value.id) + def _get_mto_route(self): + mto_route = self.env.ref('stock.route_warehouse0_mto', raise_if_not_found=False) + if not mto_route: + mto_route = self.env['stock.location.route'].search([('name', 'like', _('Make To Order'))], limit=1) + if not mto_route: raise UserError(_('Can\'t find any generic Make To Order route.')) - return mto_route_id + return mto_route - def _get_mto_pull_rule(self, cr, uid, ids, values, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - mto_route_id = self._get_mto_route(cr, uid, context=context) - res = [] - for value in values: - from_loc, dest_loc, pick_type_id = value - res += [{ - 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'), - 'location_src_id': from_loc.id, - 'location_id': dest_loc.id, - 'route_id': mto_route_id, - 'action': 'move', - 'picking_type_id': pick_type_id, - 'procure_method': 'make_to_order', - 'active': True, - 'warehouse_id': warehouse.id, - }] - return res - - def _get_crossdock_route(self, cr, uid, ids, route_name, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] + def _get_inter_warehouse_route_values(self, supplier_warehouse): return { - 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), + 'name': _('%s: Supply Product from %s') % (self.name, supplier_warehouse.name), 'warehouse_selectable': False, 'product_selectable': True, 'product_categ_selectable': True, - 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', - 'sequence': 20, - } + 'supplied_wh_id': self.id, + 'supplier_wh_id': supplier_warehouse.id} + _get_inter_wh_route = _get_inter_warehouse_route_values # compatibility - def create_routes(self, cr, uid, ids, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - wh_route_ids = [] - route_obj = self.pool.get('stock.location.route') - pull_obj = self.pool.get('procurement.rule') - push_obj = self.pool.get('stock.location.path') - routes_dict = warehouse.get_routes_dict() - #create reception route and rules - route_name, values = routes_dict[warehouse.reception_steps] - route_vals = warehouse._get_reception_delivery_route(route_name) - reception_route_id = route_obj.create(cr, uid, route_vals, context=context) - wh_route_ids.append((4, reception_route_id)) - push_rules_list, pull_rules_list = warehouse._get_push_pull_rules(True, values, reception_route_id) - #create the push/procurement rules - for push_rule in push_rules_list: - push_obj.create(cr, uid, vals=push_rule, context=context) - for pull_rule in pull_rules_list: - #all procurement rules in reception route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location - pull_rule['procure_method'] = 'make_to_order' - pull_obj.create(cr, uid, vals=pull_rule, context=context) - - #create MTS route and procurement rules for delivery and a specific route MTO to be set on the product - route_name, values = routes_dict[warehouse.delivery_steps] - route_vals = warehouse._get_reception_delivery_route(route_name) - #create the route and its procurement rules - delivery_route_id = route_obj.create(cr, uid, route_vals, context=context) - wh_route_ids.append((4, delivery_route_id)) - dummy, pull_rules_list = warehouse._get_push_pull_rules(True, values, delivery_route_id) - for pull_rule in pull_rules_list: - pull_obj.create(cr, uid, vals=pull_rule, context=context) - #create MTO procurement rule and link it to the generic MTO route - mto_pull_vals = warehouse._get_mto_pull_rule(values)[0] - mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context) - - #create a route for cross dock operations, that can be set on products and product categories - route_name, values = routes_dict['crossdock'] - crossdock_route_vals = warehouse._get_crossdock_route(route_name) - crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context) - wh_route_ids.append((4, crossdock_route_id)) - dummy, pull_rules_list = warehouse._get_push_pull_rules(warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', values, crossdock_route_id) - for pull_rule in pull_rules_list: - # Fixed cross-dock is logically mto - pull_rule['procure_method'] = 'make_to_order' - pull_obj.create(cr, uid, vals=pull_rule, context=context) - - #create route selectable on the product to resupply the warehouse from another one - warehouse._create_resupply_routes(warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id) - - #return routes and mto procurement rule to store on the warehouse + def _get_crossdock_route_values(self): return { - 'route_ids': wh_route_ids, - 'mto_pull_id': mto_pull_id, - 'reception_route_id': reception_route_id, - 'delivery_route_id': delivery_route_id, - 'crossdock_route_id': crossdock_route_id, - } + 'name': self._format_routename(route_type='crossdock'), + 'warehouse_selectable': False, + 'product_selectable': True, + 'product_categ_selectable': True, + 'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step', + 'sequence': 20} - def change_route(self, cr, uid, ids, new_reception_step=False, new_delivery_step=False, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - picking_type_obj = self.pool.get('stock.picking.type') - pull_obj = self.pool.get('procurement.rule') - push_obj = self.pool.get('stock.location.path') - route_obj = self.pool.get('stock.location.route') - new_reception_step = new_reception_step or warehouse.reception_steps - new_delivery_step = new_delivery_step or warehouse.delivery_steps + def _get_crossdock_route(self, route_name): # compatibility + return self._get_crossdock_route_values() - #change the default source and destination location and (de)activate picking types - input_loc = warehouse.wh_input_stock_loc_id - if new_reception_step == 'one_step': - input_loc = warehouse.lot_stock_id - output_loc = warehouse.wh_output_stock_loc_id - if new_delivery_step == 'ship_only': - output_loc = warehouse.lot_stock_id - picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context) - picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context) - picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, { - 'active': new_delivery_step != 'ship_only', - 'default_location_dest_id': output_loc.id if new_delivery_step == 'pick_ship' else warehouse.wh_pack_stock_loc_id.id, - }, context=context) - picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context) + # Pull / Push tools + # ------------------------------------------------------------ - routes_dict = warehouse.get_routes_dict() - #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it - pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context) - route_name, values = routes_dict[new_delivery_step] - route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context) - dummy, pull_rules_list = warehouse._get_push_pull_rules(True, values, warehouse.delivery_route_id.id) - #create the procurement rules - for pull_rule in pull_rules_list: - pull_obj.create(cr, uid, vals=pull_rule, context=context) + @api.multi + def _get_push_pull_rules_values(self, route_values, values=None, push_values=None, pull_values=None, name_suffix=''): + first_rule = True + push_rules_list, pull_rules_list = [], [] + for routing in route_values: + route_push_values = { + 'name': self._format_rulename(routing.from_loc, routing.dest_loc, name_suffix), + 'location_from_id': routing.from_loc.id, + 'location_dest_id': routing.dest_loc.id, + 'auto': 'manual', + 'picking_type_id': routing.picking_type.id, + 'warehouse_id': self.id} + route_push_values.update((values or {}).items() + (push_values or {}).items()) + push_rules_list.append(route_push_values) + route_pull_values = { + 'name': self._format_rulename(routing.from_loc, routing.dest_loc, name_suffix), + 'location_src_id': routing.from_loc.id, + 'location_id': routing.dest_loc.id, + 'action': 'move', + 'picking_type_id': routing.picking_type.id, + 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order', + 'warehouse_id': self.id} + route_pull_values.update((values or {}).items() + (pull_values or {}).items()) + pull_rules_list.append(route_pull_values) + first_rule = False + return push_rules_list, pull_rules_list - #update receipt route and rules: unlink the existing rules of the warehouse receipt route and recreate it - pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context) - push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context) - route_name, values = routes_dict[new_reception_step] - route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context) - push_rules_list, pull_rules_list = warehouse._get_push_pull_rules(True, values, warehouse.reception_route_id.id) - #create the push/procurement rules - for push_rule in push_rules_list: - push_obj.create(cr, uid, vals=push_rule, context=context) - for pull_rule in pull_rules_list: - #all procurement rules in receipt route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location - pull_rule['procure_method'] = 'make_to_order' - pull_obj.create(cr, uid, vals=pull_rule, context=context) + def _get_mto_pull_rules_values(self, route_values): + mto_route = self._get_mto_route() + dummy, pull_rules_list = self._get_push_pull_rules_values(route_values, pull_values={ + 'route_id': mto_route.id, + 'procure_method': 'make_to_order', + 'active': True}, name_suffix=_('MTO')) + return pull_rules_list + _get_mto_pull_rule = _get_mto_pull_rules_values # compatibility - route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context) + def _get_push_pull_rules(self, active, values, new_route_id): # compatibility + return self._get_push_pull_rules_values(values, values={'active': active, 'route_id': new_route_id}) - #change MTO rule - dummy, values = routes_dict[new_delivery_step] - mto_pull_vals = warehouse._get_mto_pull_rule(values)[0] - pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context) - return True + def _get_supply_pull_rules_values(self, route_values, values=None): + dummy, pull_rules_list = self._get_push_pull_rules_values(route_values, values=values, pull_values={'active': True}) + for pull_rules in pull_rules_list: + 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 + return pull_rules_list - def create_sequences_and_picking_types(self, cr, uid, ids, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - seq_obj = self.pool.get('ir.sequence') - picking_type_obj = self.pool.get('stock.picking.type') - #create new sequences - in_seq_id = seq_obj.create(cr, SUPERUSER_ID, {'name': warehouse.name + _(' Sequence in'), 'prefix': warehouse.code + '/IN/', 'padding': 5}, context=context) - out_seq_id = seq_obj.create(cr, SUPERUSER_ID, {'name': warehouse.name + _(' Sequence out'), 'prefix': warehouse.code + '/OUT/', 'padding': 5}, context=context) - pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, {'name': warehouse.name + _(' Sequence packing'), 'prefix': warehouse.code + '/PACK/', 'padding': 5}, context=context) - pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, {'name': warehouse.name + _(' Sequence picking'), 'prefix': warehouse.code + '/PICK/', 'padding': 5}, context=context) - int_seq_id = seq_obj.create(cr, SUPERUSER_ID, {'name': warehouse.name + _(' Sequence internal'), 'prefix': warehouse.code + '/INT/', 'padding': 5}, context=context) + def _update_reception_delivery_resupply(self, reception_new, delivery_new): + """ Check if we need to change something to resupply warehouses and associated MTO rules """ + input_loc, output_loc = self._get_input_output_locations(reception_new, delivery_new) + for warehouse in self: + if reception_new and warehouse.reception_steps != reception_new and (warehouse.reception_steps == 'one_step' or reception_new == 'one_step'): + warehouse._check_reception_resupply(input_loc) + if delivery_new and warehouse.delivery_steps != delivery_new and (warehouse.delivery_steps == 'ship_only' or delivery_new == 'ship_only'): + change_to_multiple = warehouse.delivery_steps == 'ship_only' + warehouse._check_delivery_resupply(output_loc, change_to_multiple) + _check_resupply = _update_reception_delivery_resupply - wh_stock_loc = warehouse.lot_stock_id - wh_input_stock_loc = warehouse.wh_input_stock_loc_id - wh_output_stock_loc = warehouse.wh_output_stock_loc_id - wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id - - #create in, out, internal picking types for warehouse - input_loc = wh_input_stock_loc - if warehouse.reception_steps == 'one_step': - input_loc = wh_stock_loc - output_loc = wh_output_stock_loc - if warehouse.delivery_steps == 'ship_only': - output_loc = wh_stock_loc - - #choose the next available color for the picking types of this warehouse - color = 0 - available_colors = [0, 3, 4, 5, 6, 7, 8, 1, 2] # put white color first - all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color') - #don't use sets to preserve the list order - for x in all_used_colors: - if x['color'] in available_colors: - available_colors.remove(x['color']) - if available_colors: - color = available_colors[0] - - #order the picking types with a sequence allowing to have the following suit for each warehouse: reception, internal, pick, pack, ship. - max_sequence = self.pool.get('stock.picking.type').search_read(cr, uid, [], ['sequence'], order='sequence desc') - max_sequence = max_sequence and max_sequence[0]['sequence'] or 0 - internal_active_false = (warehouse.reception_steps == 'one_step') and (warehouse.delivery_steps == 'ship_only') - internal_active_false = internal_active_false and not self.user_has_groups(cr, uid, 'stock.group_stock_multi_locations') - - in_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Receipts'), - 'warehouse_id': warehouse.id, - 'code': 'incoming', - 'use_create_lots': True, - 'use_existing_lots': False, - 'sequence_id': in_seq_id, - 'default_location_src_id': False, - 'default_location_dest_id': input_loc.id, - 'sequence': max_sequence + 1, - 'color': color}, context=context) - out_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Delivery Orders'), - 'warehouse_id': warehouse.id, - 'code': 'outgoing', - 'use_create_lots': False, - 'use_existing_lots': True, - 'sequence_id': out_seq_id, - 'return_picking_type_id': in_type_id, - 'default_location_src_id': output_loc.id, - 'default_location_dest_id': False, - 'sequence': max_sequence + 4, - 'color': color}, context=context) - picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context) - int_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Internal Transfers'), - 'warehouse_id': warehouse.id, - 'code': 'internal', - 'use_create_lots': False, - 'use_existing_lots': True, - 'sequence_id': int_seq_id, - 'default_location_src_id': wh_stock_loc.id, - 'default_location_dest_id': wh_stock_loc.id, - 'active': not internal_active_false, - 'sequence': max_sequence + 2, - 'color': color}, context=context) - pack_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Pack'), - 'warehouse_id': warehouse.id, - 'code': 'internal', - 'use_create_lots': False, - 'use_existing_lots': True, - 'sequence_id': pack_seq_id, - 'default_location_src_id': wh_pack_stock_loc.id, - 'default_location_dest_id': output_loc.id, - 'active': warehouse.delivery_steps == 'pick_pack_ship', - 'sequence': max_sequence + 3, - 'color': color}, context=context) - pick_type_id = picking_type_obj.create(cr, uid, vals={ - 'name': _('Pick'), - 'warehouse_id': warehouse.id, - 'code': 'internal', - 'use_create_lots': False, - 'use_existing_lots': True, - 'sequence_id': pick_seq_id, - 'default_location_src_id': wh_stock_loc.id, - 'default_location_dest_id': output_loc.id if warehouse.delivery_steps == 'pick_ship' else wh_pack_stock_loc.id, - 'active': warehouse.delivery_steps != 'ship_only', - 'sequence': max_sequence + 2, - 'color': color}, context=context) - - #write picking types on WH - vals = { - 'in_type_id': in_type_id, - 'out_type_id': out_type_id, - 'pack_type_id': pack_type_id, - 'pick_type_id': pick_type_id, - 'int_type_id': int_type_id, - } - super(stock_warehouse, self).write(cr, uid, warehouse.id, vals=vals, context=context) - - def create(self, cr, uid, vals, context=None): - if context is None: - context = {} - if vals is None: - vals = {} - data_obj = self.pool.get('ir.model.data') - seq_obj = self.pool.get('ir.sequence') - picking_type_obj = self.pool.get('stock.picking.type') - location_obj = self.pool.get('stock.location') - - #create view location for warehouse - loc_vals = { - 'name': _(vals.get('code')), - 'usage': 'view', - 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1], - } - if vals.get('company_id'): - loc_vals['company_id'] = vals.get('company_id') - wh_loc_id = location_obj.create(cr, uid, loc_vals, context=context) - vals['view_location_id'] = wh_loc_id - #create all location - def_values = self.default_get(cr, uid, {'reception_steps', 'delivery_steps'}) - reception_steps = vals.get('reception_steps', def_values['reception_steps']) - delivery_steps = vals.get('delivery_steps', def_values['delivery_steps']) - context_with_inactive = context.copy() - context_with_inactive['active_test'] = False - sub_locations = [ - {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'}, - {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'}, - {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'}, - {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'}, - {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'}, - ] - for values in sub_locations: - loc_vals = { - 'name': values['name'], - 'usage': 'internal', - 'location_id': wh_loc_id, - 'active': values['active'], - } - if vals.get('company_id'): - loc_vals['company_id'] = vals.get('company_id') - location_id = location_obj.create(cr, uid, loc_vals, context=context_with_inactive) - vals[values['field']] = location_id - - #create WH - new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context) - warehouse = self.browse(cr, uid, new_id, context=context) - self.create_sequences_and_picking_types(cr, uid, [warehouse.id], context=context) - - #create routes and push/procurement rules - new_objects_dict = self.create_routes(cr, uid, [new_id], context=context) - self.write(cr, uid, warehouse.id, new_objects_dict, context=context) - - # If partner assigned - if vals.get('partner_id'): - comp_obj = self.pool['res.company'] - if vals.get('company_id'): - transit_loc = comp_obj.browse(cr, uid, vals.get('company_id'), context=context).internal_transit_location_id.id - else: - transit_loc = comp_obj.browse(cr, uid, comp_obj._company_default_get(cr, uid, 'stock.warehouse', context=context)).internal_transit_location_id.id - self.pool['res.partner'].write(cr, uid, [vals['partner_id']], {'property_stock_customer': transit_loc, - 'property_stock_supplier': transit_loc}, context=context) - return new_id - - def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None): - return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name - - def _format_routename(self, cr, uid, obj, name, context=None): - return obj.name + ': ' + name - - def get_routes_dict(self, cr, uid, ids, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - #fetch customer and supplier locations, for references - customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context) - - return { - 'one_step': (_('Receipt in 1 step'), []), - 'two_steps': (_('Receipt in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]), - 'three_steps': (_('Receipt in 3 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]), - 'crossdock': (_('Cross-Dock'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]), - 'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]), - 'pick_ship': (_('Pick + Ship'), [(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]), - 'pick_pack_ship': (_('Pick + Pack + Ship'), [(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id.id), (warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]), - } - - def _handle_renaming(self, cr, uid, ids, name, code, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - location_obj = self.pool.get('stock.location') - route_obj = self.pool.get('stock.location.route') - pull_obj = self.pool.get('procurement.rule') - push_obj = self.pool.get('stock.location.path') - #rename location - location_id = warehouse.lot_stock_id.location_id.id - location_obj.write(cr, uid, location_id, {'name': code}, context=context) - #rename route and push-procurement rules - for route in warehouse.route_ids: - route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context) - for pull in route.pull_ids: - pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context) - for push in route.push_ids: - push_obj.write(cr, uid, push.id, {'name': push.name.replace(warehouse.name, name, 1)}, context=context) - #change the mto procurement rule name - if warehouse.mto_pull_id.id: - pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context) - - def _check_delivery_resupply(self, cr, uid, ids, new_location, change_to_multiple, context=None): - """ Will check if the resupply routes from this warehouse follow the changes of number of delivery steps """ - warehouse = self.browse(cr, uid, ids[0], context=context) - #Check routes that are being delivered by this warehouse and change the rule going to transit location - route_obj = self.pool.get("stock.location.route") - pull_obj = self.pool.get("procurement.rule") - routes = route_obj.search(cr, uid, [('supplier_wh_id','=', warehouse.id)], context=context) - pulls = pull_obj.search(cr, uid, ['&', ('route_id', 'in', routes), ('location_id.usage', '=', 'transit')], context=context) - if pulls: - pull_obj.write(cr, uid, pulls, {'location_src_id': new_location, 'procure_method': change_to_multiple and "make_to_order" or "make_to_stock"}, context=context) - # Create or clean MTO rules - mto_route_id = self._get_mto_route(cr, uid, context=context) + def _check_delivery_resupply(self, new_location, change_to_multiple): + """ Check if the resupply routes from this warehouse follow the changes of number of delivery steps + Check routes being delivery bu this warehouse and change the rule going to transit location """ + Pull = self.env["procurement.rule"] + routes = self.env['stock.location.route'].search([('supplier_wh_id', '=', self.id)]) + pulls = Pull.search(['&', ('route_id', 'in', routes.ids), ('location_id.usage', '=', 'transit')]) + pulls.write({ + 'location_src_id': new_location, + 'procure_method': change_to_multiple and "make_to_order" or "make_to_stock"}) if not change_to_multiple: - # If single delivery we should create the necessary MTO rules for the resupply - # pulls = pull_obj.search(cr, uid, ['&', ('route_id', '=', mto_route_id), ('location_id.usage', '=', 'transit'), ('location_src_id', '=', warehouse.lot_stock_id.id)], context=context) - pull_recs = pull_obj.browse(cr, uid, pulls, context=context) - transfer_locs = list(set([x.location_id for x in pull_recs])) - vals = [(warehouse.lot_stock_id , x, warehouse.out_type_id.id) for x in transfer_locs] - mto_pull_vals = warehouse._get_mto_pull_rule(vals) + # If single delivery we should create the necessary MTO rules for the resupply + routings = [self.Routing(self.lot_stock_id , location, self.out_type_id) for location in pulls.mapped('location_id')] + mto_pull_vals = self._get_mto_pull_rules_values(routings) for mto_pull_val in mto_pull_vals: - pull_obj.create(cr, uid, mto_pull_val, context=context) + Pull.create(mto_pull_val) else: # We need to delete all the MTO procurement rules, otherwise they risk to be used in the system - pulls = pull_obj.search(cr, uid, ['&', ('route_id', '=', mto_route_id), ('location_id.usage', '=', 'transit'), ('location_src_id', '=', warehouse.lot_stock_id.id)], context=context) - if pulls: - pull_obj.unlink(cr, uid, pulls, context=context) + Pull.search([ + '&', ('route_id', '=', self._get_mto_route().id), + ('location_id.usage', '=', 'transit'), + ('location_src_id', '=', self.lot_stock_id.id)]).unlink() - def _check_reception_resupply(self, cr, uid, ids, new_location, context=None): - """ - Will check if the resupply routes to this warehouse follow the changes of number of receipt steps - """ - warehouse = self.browse(cr, uid, ids[0], context=context) - #Check routes that are being delivered by this warehouse and change the rule coming from transit location - route_obj = self.pool.get("stock.location.route") - pull_obj = self.pool.get("procurement.rule") - routes = route_obj.search(cr, uid, [('supplied_wh_id','=', warehouse.id)], context=context) - pulls= pull_obj.search(cr, uid, ['&', ('route_id', 'in', routes), ('location_src_id.usage', '=', 'transit')]) + def _check_reception_resupply(self, new_location): + """ Check routes being delivered by the warehouses (resupply routes) and + change their rule coming from the transit location """ + routes = self.env['stock.location.route'].search([('supplied_wh_id', 'in', self.ids)]) + self.env['procurement.rule'].search([ + '&', ('route_id', 'in', routes.ids), + ('location_src_id.usage', '=', 'transit')]).write({'location_id': new_location}) + + @api.multi + def _update_routes(self): + routes_data = self.get_routes_dict() + # change the default source and destination location and (de)activate picking types + self._update_picking_type() + # update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it + self._create_or_update_delivery_route(routes_data) + # update receipt route and rules: unlink the existing rules of the warehouse receipt route and recreate it + self._create_or_update_reception_route(routes_data) + self._create_or_update_crossdock_route(routes_data) + self._create_or_update_mto_pull(routes_data) + return True + change_route = _update_routes + + @api.one + def _update_picking_type(self): + picking_type_values = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id) + for field_name, values in picking_type_values.iteritems(): + getattr(self, field_name).write(values) + + @api.multi + def _update_name_and_code(self, new_name=False, new_code=False): + if new_code: + self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code}) + if new_name: + # TDE FIXME: replacing the route name ? not better to re-generate the route naming ? + for warehouse in self: + routes = warehouse.route_ids + for route in routes: + route.write({'name': route.name.replace(warehouse.name, new_name, 1)}) + for pull in route.pull_ids: + pull.write({'name': pull.name.replace(warehouse.name, new_name, 1)}) + for push in route.push_ids: + push.write({'name': push.name.replace(warehouse.name, new_name, 1)}) + warehouse.mto_pull_id.write({'name': warehouse.mto_pull_id.name.replace(warehouse.name, new_name, 1)}) + for warehouse in self: + sequence_data = warehouse._get_sequence_values() + warehouse.in_type_id.sequence_id.write(sequence_data['in_type_id']) + warehouse.out_type_id.sequence_id.write(sequence_data['out_type_id']) + warehouse.pack_type_id.sequence_id.write(sequence_data['pack_type_id']) + warehouse.pick_type_id.sequence_id.write(sequence_data['pick_type_id']) + warehouse.int_type_id.sequence_id.write(sequence_data['int_type_id']) + _handle_renaming = _update_name_and_code + + def _update_location_reception(self, new_reception_step): + switch_warehouses = self.filtered(lambda wh: wh.reception_steps != new_reception_step and not wh._location_used(wh.wh_input_stock_loc_id)) + if switch_warehouses: + (switch_warehouses.mapped('wh_input_stock_loc_id') + switch_warehouses.mapped('wh_qc_stock_loc_id')).write({'active': False}) + if new_reception_step == 'three_steps': + self.mapped('wh_qc_stock_loc_id').write({'active': True}) + if new_reception_step != 'one_step': + self.mapped('wh_input_stock_loc_id').write({'active': True}) + + def _update_location_delivery(self, new_delivery_step): + switch_warehouses = self.filtered(lambda wh: wh.delivery_steps != new_delivery_step) + loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.wh_output_stock_loc_id)) + if loc_warehouse: + loc_warehouse.mapped('wh_output_stock_loc_id').write({'active': False}) + loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.wh_pack_stock_loc_id)) + if loc_warehouse: + loc_warehouse.mapped('wh_pack_stock_loc_id').write({'active': False}) + if new_delivery_step == 'pick_pack_ship': + self.mapped('wh_pack_stock_loc_id').write({'active': True}) + if new_delivery_step != 'ship_only': + self.mapped('wh_output_stock_loc_id').write({'active': True}) + + def _location_used(self, location): + pulls = self.env['procurement.rule'].search_count([ + '&', + ('route_id', 'not in', [x.id for x in self.route_ids]), + '|', ('location_src_id', '=', location.id), + ('location_id', '=', location.id)]) if pulls: - pull_obj.write(cr, uid, pulls, {'location_id': new_location}, context=context) + return True + pushs = self.env['stock.location.path'].search_count([ + '&', + ('route_id', 'not in', [x.id for x in self.route_ids]), + '|', ('location_from_id', '=', location.id), + ('location_dest_id', '=', location.id)]) + if pushs: + return True + return False - def _check_resupply(self, cr, uid, ids, reception_new, delivery_new, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - if reception_new: - old_val = warehouse.reception_steps - new_val = reception_new - change_to_one = (old_val != 'one_step' and new_val == 'one_step') - change_to_multiple = (old_val == 'one_step' and new_val != 'one_step') - if change_to_one or change_to_multiple: - new_location = change_to_one and warehouse.lot_stock_id.id or warehouse.wh_input_stock_loc_id.id - self._check_reception_resupply(cr, uid, [warehouse.id], new_location, context=context) - if delivery_new: - old_val = warehouse.delivery_steps - new_val = delivery_new - change_to_one = (old_val != 'ship_only' and new_val == 'ship_only') - change_to_multiple = (old_val == 'ship_only' and new_val != 'ship_only') - if change_to_one or change_to_multiple: - new_location = change_to_one and warehouse.lot_stock_id.id or warehouse.wh_output_stock_loc_id.id - self._check_delivery_resupply(cr, uid, [warehouse.id], new_location, change_to_multiple, context=context) + # Misc + # ------------------------------------------------------------ - def write(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - if isinstance(ids, (int, long)): - ids = [ids] - seq_obj = self.pool.get('ir.sequence') - route_obj = self.pool.get('stock.location.route') - context_with_inactive = context.copy() - context_with_inactive['active_test'] = False - for warehouse in self.browse(cr, uid, ids, context=context_with_inactive): - #first of all, check if we need to delete and recreate route - if vals.get('reception_steps') or vals.get('delivery_steps'): - #activate and deactivate location according to reception and delivery option - warehouse.switch_location(vals.get('reception_steps', False), vals.get('delivery_steps', False)) - # switch between route - self.change_route(cr, uid, [warehouse.id], vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive) - # Check if we need to change something to resupply warehouses and associated MTO rules - warehouse._check_resupply(vals.get('reception_steps'), vals.get('delivery_steps')) - if vals.get('code') or vals.get('name'): - name = warehouse.name - #rename sequence - if vals.get('name'): - name = vals.get('name', warehouse.name) - self._handle_renaming(cr, uid, [warehouse.id], name, vals.get('code', warehouse.code), context=context_with_inactive) - if warehouse.in_type_id: - seq_obj.write(cr, uid, [warehouse.in_type_id.sequence_id.id], {'name': name + _(' Sequence in'), 'prefix': vals.get('code', warehouse.code) + '\IN\\'}, context=context) - if warehouse.out_type_id: - seq_obj.write(cr, uid, [warehouse.out_type_id.sequence_id.id], {'name': name + _(' Sequence out'), 'prefix': vals.get('code', warehouse.code) + '\OUT\\'}, context=context) - if warehouse.pack_type_id: - seq_obj.write(cr, uid, [warehouse.pack_type_id.sequence_id.id], {'name': name + _(' Sequence packing'), 'prefix': vals.get('code', warehouse.code) + '\PACK\\'}, context=context) - if warehouse.pick_type_id: - seq_obj.write(cr, uid, [warehouse.pick_type_id.sequence_id.id], {'name': name + _(' Sequence picking'), 'prefix': vals.get('code', warehouse.code) + '\PICK\\'}, context=context) - if warehouse.int_type_id: - seq_obj.write(cr, uid, [warehouse.int_type_id.sequence_id.id], {'name': name + _(' Sequence internal'), 'prefix': vals.get('code', warehouse.code) + '\INT\\'}, context=context) - if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'): - for cmd in vals.get('resupply_wh_ids'): - if cmd[0] == 6: - new_ids = set(cmd[2]) - old_ids = set([wh.id for wh in warehouse.resupply_wh_ids]) - to_add_wh_ids = new_ids - old_ids - if to_add_wh_ids: - supplier_warehouses = self.browse(cr, uid, list(to_add_wh_ids), context=context) - warehouse._create_resupply_routes(supplier_warehouses, warehouse.default_resupply_wh_id) - to_remove_wh_ids = old_ids - new_ids - if to_remove_wh_ids: - to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', 'in', list(to_remove_wh_ids))], context=context) - if to_remove_route_ids: - route_obj.unlink(cr, uid, to_remove_route_ids, context=context) - else: - #not implemented - pass - if 'default_resupply_wh_id' in vals: - if vals.get('default_resupply_wh_id') == warehouse.id: - raise UserError(_('The default resupply warehouse should be different than the warehouse itself!')) - if warehouse.default_resupply_wh_id: - #remove the existing resupplying route on the warehouse - to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', warehouse.default_resupply_wh_id.id)], context=context) - for inter_wh_route_id in to_remove_route_ids: - self.write(cr, uid, [warehouse.id], {'route_ids': [(3, inter_wh_route_id)]}) - if vals.get('default_resupply_wh_id'): - #assign the new resupplying route on all products - to_assign_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', vals.get('default_resupply_wh_id'))], context=context) - for inter_wh_route_id in to_assign_route_ids: - self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}) + def _get_picking_type_values(self, reception_steps, delivery_steps, pack_stop_location): + input_loc, output_loc = self._get_input_output_locations(reception_steps, delivery_steps) + return { + 'in_type_id': {'default_location_dest_id': input_loc.id}, + 'out_type_id': {'default_location_src_id': output_loc.id}, + 'pick_type_id': { + 'active': delivery_steps != 'ship_only', + 'default_location_dest_id': output_loc.id if delivery_steps == 'pick_ship' else pack_stop_location.id}, + 'pack_type_id': {'active': delivery_steps == 'pick_pack_ship'}, + 'int_type_id': {}, + } - # If another partner assigned - if vals.get('partner_id'): - if not vals.get('company_id'): - company = self.browse(cr, uid, ids[0], context=context).company_id - else: - company = self.pool['res.company'].browse(cr, uid, vals['company_id']) - transit_loc = company.internal_transit_location_id.id - self.pool['res.partner'].write(cr, uid, [vals['partner_id']], {'property_stock_customer': transit_loc, - 'property_stock_supplier': transit_loc}, context=context) - return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context) + def _get_sequence_values(self): + return { + 'in_type_id': {'name': self.name + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5}, + 'out_type_id': {'name': self.name + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5}, + 'pack_type_id': {'name': self.name + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5}, + 'pick_type_id': {'name': self.name + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5}, + 'int_type_id': {'name': self.name + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5}, + } - def get_all_routes_for_wh(self, cr, uid, ids, context=None): - warehouse = self.browse(cr, uid, ids[0], context=context) - route_obj = self.pool.get("stock.location.route") - all_routes = [route.id for route in warehouse.route_ids] - all_routes += route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id)], context=context) - all_routes += [warehouse.mto_pull_id.route_id.id] - return all_routes + @api.multi + def _format_rulename(self, from_loc, dest_loc, suffix): + return '%s: %s -> %s%s' % (self.code, from_loc.name, dest_loc.name, suffix) - def action_view_all_routes(self, cr, uid, ids, context=None): - all_routes = [] - for wh in self.browse(cr, uid, ids, context=context): - all_routes += wh.get_all_routes_for_wh() + @api.multi + def _format_routename(self, name=None, route_type=None): + if route_type: + name = self._get_route_name(route_type) + return '%s: %s' % (self.name, name) - domain = [('id', 'in', all_routes)] + @api.returns('self') + @api.multi + def _get_all_routes(self): + # TDE FIXME: check overrides + routes = self.mapped('route_ids') | self.mapped('mto_pull_id').mapped('route_id') + routes |= self.env["stock.location.route"].search([('supplied_wh_id', 'in', self.ids)]) + return routes + get_all_routes_for_wh = _get_all_routes + + @api.multi + def action_view_all_routes(self): + routes = self._get_all_routes() return { 'name': _('Warehouse\'s Routes'), - 'domain': domain, + 'domain': [('id', 'in', routes.ids)], 'res_model': 'stock.location.route', 'type': 'ir.actions.act_window', 'view_id': False, @@ -774,157 +696,136 @@ class stock_warehouse(osv.osv): } -class stock_warehouse_orderpoint(osv.osv): - """ - Defines Minimum stock rules. - """ +class Orderpoint(models.Model): + """ Defines Minimum stock rules. """ _name = "stock.warehouse.orderpoint" _description = "Minimum Inventory Rule" - def subtract_procurements_from_orderpoints(self, cr, uid, orderpoint_ids, context=None): - '''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it. - ''' - - cr.execute("""select op.id, p.id, p.product_uom, p.product_qty, pt.uom_id, sm.product_qty from procurement_order as p left join stock_move as sm ON sm.procurement_id = p.id, - stock_warehouse_orderpoint op, product_product pp, product_template pt - WHERE p.orderpoint_id = op.id AND p.state not in ('done', 'cancel') AND (sm.state IS NULL OR sm.state not in ('draft')) - AND pp.id = p.product_id AND pp.product_tmpl_id = pt.id - AND op.id IN %s - ORDER BY op.id, p.id - """, (tuple(orderpoint_ids),)) - results = cr.fetchall() - current_proc = False - current_op = False - uom_obj = self.pool.get("product.uom") - op_qty = 0 - res = dict.fromkeys(orderpoint_ids, 0.0) - for move_result in results: - op = move_result[0] - if current_op != op: - if current_op: - res[current_op] = op_qty - current_op = op - op_qty = 0 - proc = move_result[1] - if proc != current_proc: - op_qty += uom_obj._compute_qty(cr, uid, move_result[2], move_result[3], move_result[4], round=False) - current_proc = proc - if move_result[5]: #If a move is associated (is move qty) - op_qty -= move_result[5] - if current_op: - res[current_op] = op_qty + @api.model + def default_get(self, fields): + res = super(Orderpoint, self).default_get(fields) + warehouse = None + if 'warehouse_id' not in res and res.get('company_id'): + warehouse = self.env['stock.warehouse'].search([('company_id', '=', res['company_id'])], limit=1) + if warehouse: + res['warehouse_id'] = warehouse.id + res['location_id'] = warehouse.lot_stock_id.id return res - def _check_product_uom(self, cr, uid, ids, context=None): - ''' - Check if the UoM has the same category as the product standard UoM - ''' - if not context: - context = {} + name = fields.Char( + 'Name', copy=False, required=True, + default=lambda self: self.env['ir.sequence'].next_by_code('stock.orderpoint')) + active = fields.Boolean( + 'Active', default=True, + help="If the active field is set to False, it will allow you to hide the orderpoint without removing it.") + warehouse_id = fields.Many2one( + 'stock.warehouse', 'Warehouse', + ondelete="cascade", required=True) + location_id = fields.Many2one( + 'stock.location', 'Location', + ondelete="cascade", required=True) + product_id = fields.Many2one( + 'product.product', 'Product', + domain=[('type', '=', 'product')], ondelete='cascade', required=True) + product_uom = fields.Many2one( + 'product.uom', 'Product Unit of Measure', related='product_id.uom_id', + readonly=True, required=True, + default=lambda self: self._context.get('product_uom', False)) + product_min_qty = fields.Float( + 'Minimum Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, + help="When the virtual stock goes below the Min Quantity specified for this field, Odoo generates " + "a procurement to bring the forecasted quantity to the Max Quantity.") + product_max_qty = fields.Float( + 'Maximum Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, + help="When the virtual stock goes below the Min Quantity, Odoo generates " + "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity.") + qty_multiple = fields.Float( + 'Qty Multiple', digits_compute=dp.get_precision('Product Unit of Measure'), + default=1, required=True, + help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used.") + procurement_ids = fields.One2many('procurement.order', 'orderpoint_id', 'Created Procurements') + group_id = fields.Many2one( + 'procurement.group', 'Procurement Group', copy=False, + help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking.") + company_id = fields.Many2one( + 'res.company', 'Company', required=True, + default=lambda self: self.env['res.company']._company_default_get('stock.warehouse.orderpoint')) + lead_days = fields.Integer( + 'Lead Time', default=1, + help="Number of days after the orderpoint is triggered to receive the products or to order to the vendor") + lead_type = fields.Selection( + [('net', 'Day(s) to get the products'), ('supplier', 'Day(s) to purchase')], 'Lead Type', + required=True, default='supplier') - for rule in self.browse(cr, uid, ids, context=context): - if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id: - return False - return True - - _columns = { - 'name': fields.char('Name', required=True, copy=False), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."), - 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"), - 'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"), - 'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '=', 'product')]), - 'product_uom': fields.related('product_id', 'uom_id', type='many2one', relation='product.uom', string='Product Unit of Measure', readonly=True, required=True), - 'product_min_qty': fields.float('Minimum Quantity', required=True, - digits_compute=dp.get_precision('Product Unit of Measure'), - help="When the virtual stock goes below the Min Quantity specified for this field, Odoo generates "\ - "a procurement to bring the forecasted quantity to the Max Quantity."), - 'product_max_qty': fields.float('Maximum Quantity', required=True, - digits_compute=dp.get_precision('Product Unit of Measure'), - help="When the virtual stock goes below the Min Quantity, Odoo generates "\ - "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."), - 'qty_multiple': fields.float('Qty Multiple', required=True, - digits_compute=dp.get_precision('Product Unit of Measure'), - help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used. "), - 'procurement_ids': fields.one2many('procurement.order', 'orderpoint_id', 'Created Procurements'), - 'group_id': fields.many2one('procurement.group', 'Procurement Group', help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking.", copy=False), - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'lead_days': fields.integer('Lead Time', help="Number of days after the orderpoint is triggered to receive the products or to order to the vendor"), - 'lead_type': fields.selection([ - ('net', 'Day(s) to get the products'), - ('supplier', 'Day(s) to purchase') - ], 'Lead Type', required=True) - } - _defaults = { - 'active': lambda *a: 1, - 'lead_days': lambda *a: 1, - 'lead_type': lambda *a: 'supplier', - 'qty_multiple': lambda *a: 1, - 'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').next_by_code(cr, uid, 'stock.orderpoint') or '', - 'product_uom': lambda self, cr, uid, context: context.get('product_uom', False), - 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context) - } _sql_constraints = [ ('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'), ] - _constraints = [ - (_check_product_uom, 'You have to select a product unit of measure in the same category than the default unit of measure of the product', ['product_id', 'product_uom']), - ] - def default_get(self, cr, uid, fields, context=None): - warehouse_obj = self.pool.get('stock.warehouse') - res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context) - # default 'warehouse_id' and 'location_id' - if 'warehouse_id' not in res: - warehouse_ids = res.get('company_id') and warehouse_obj.search(cr, uid, [('company_id', '=', res['company_id'])], limit=1, context=context) or [] - res['warehouse_id'] = warehouse_ids and warehouse_ids[0] or False - if 'location_id' not in res: - res['location_id'] = res.get('warehouse_id') and warehouse_obj.browse(cr, uid, res['warehouse_id'], context).lot_stock_id.id or False - return res + @api.constrains('product_id', 'product_uom') + def _check_product_uom(self): + ''' Check if the UoM has the same category as the product standard UoM ''' + if any(orderpoint.product_id.uom_id.category_id != orderpoint.product_uom.category_id for orderpoint in self): + raise ValidationError(_('You have to select a product unit of measure in the same category than the default unit of measure of the product')) - def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None): - """ Finds location id for changed warehouse. - @param warehouse_id: Changed id of warehouse. - @return: Dictionary of values. - """ - if warehouse_id: - w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context) - v = {'location_id': w.lot_stock_id.id} - return {'value': v} - return {} + @api.onchange('warehouse_id') + def onchange_warehouse_id(self): + """ Finds location id for changed warehouse. """ + if self.warehouse_id: + self.location_id = self.warehoues_id.lot_stock_id.id - def onchange_product_id(self, cr, uid, ids, product_id, context=None): - """ Finds UoM for changed product. - @param product_id: Changed id of product. - @return: Dictionary of values. - """ - if product_id: - prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context) - d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]} - v = {'product_uom': prod.uom_id.id} - return {'value': v, 'domain': d} + @api.onchange('product_id') + def onchange_product_id(self): + if self.product_id: + self.product_uom = self.product_id.uom_id.id + return {'domain': {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}} return {'domain': {'product_uom': []}} - def _get_date_planned(self, cr, uid, ids, start_date, context=None): - orderpoint = self.browse(cr, uid, ids[0], context=context) - days = orderpoint.lead_days or 0.0 - if orderpoint.lead_type == 'purchase': + @api.multi + def subtract_procurements_from_orderpoints(self): + '''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it. + ''' + self._cr.execute("""SELECT orderpoint.id, procurement.id, procurement.product_uom, procurement.product_qty, template.uom_id, move.product_qty + FROM stock_warehouse_orderpoint orderpoint + JOIN procurement_order AS procurement ON procurement.orderpoint_id = orderpoint.id + JOIN product_product AS product ON product.id = procurement.product_id + JOIN product_template AS template ON template.id = product.product_tmpl_id + LEFT JOIN stock_move AS move ON move.procurement_id = procurement.id + WHERE procurement.state not in ('done', 'cancel') + AND (move.state IS NULL OR move.state != 'draft') + AND orderpoint.id IN %s + ORDER BY orderpoint.id, procurement.id + """, (tuple(self.ids),)) + UoM = self.env["product.uom"] + procurements_done = set() + res = dict.fromkeys(self.ids, 0.0) + for orderpoint_id, procurement_id, product_uom_id, procurement_qty, template_uom_id, move_qty in self._cr.fetchall(): + if procurement_id not in procurements_done: # count procurement once, if multiple move in this orderpoint/procurement combo + procurements_done.add(procurement_id) + res[orderpoint_id] += UoM._compute_qty(product_uom_id, procurement_qty, template_uom_id, round=False) + if move_qty: + res[orderpoint_id] -= move_qty + return res + + def _get_date_planned(self, start_date): + days = self.lead_days or 0.0 + if self.lead_type == 'purchase': # These days will be substracted when creating the PO - days += orderpoint.product_id._select_seller(orderpoint.product_id).delay or 0.0 + days += self.product_id._select_seller(self.product_id).delay or 0.0 date_planned = start_date + relativedelta.relativedelta(days=days) return date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT) - def _prepare_procurement_values(self, cr, uid, ids, product_qty, date=False, group=False, context=None): - orderpoint = self.browse(cr, uid, ids[0], context=context) + @api.multi + def _prepare_procurement_values(self, product_qty, date=False, group=False): return { - 'name': orderpoint.name, - 'date_planned': date or orderpoint._get_date_planned(datetime.today()), - 'product_id': orderpoint.product_id.id, + 'name': self.name, + 'date_planned': date or self._get_date_planned(datetime.today()), + 'product_id': self.product_id.id, 'product_qty': product_qty, - 'company_id': orderpoint.company_id.id, - 'product_uom': orderpoint.product_uom.id, - 'location_id': orderpoint.location_id.id, - 'origin': orderpoint.name, - 'warehouse_id': orderpoint.warehouse_id.id, - 'orderpoint_id': orderpoint.id, - 'group_id': group or orderpoint.group_id.id, + 'company_id': self.company_id.id, + 'product_uom': self.product_uom.id, + 'location_id': self.location_id.id, + 'origin': self.name, + 'warehouse_id': self.warehouse_id.id, + 'orderpoint_id': self.id, + 'group_id': group or self.group_id.id, } diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml index c3a6883482e..c13e4455c20 100644 --- a/addons/stock/views/stock_warehouse_views.xml +++ b/addons/stock/views/stock_warehouse_views.xml @@ -33,8 +33,8 @@ - - + + @@ -156,10 +156,10 @@ - + - +