diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 7120126c39e..53055cbcc05 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -76,6 +76,7 @@ Dashboard / Reports for Inventory Management will include: 'views/stock_picking_views.xml', 'views/stock_quant_views.xml', 'views/procurement_views.xml', + 'views/stock_warehouse_views.xml', 'views/product_views.xml', 'views/stock_config_settings_views.xml', diff --git a/addons/stock/models/__init__.py b/addons/stock/models/__init__.py index 62d9fb12197..e59796bb985 100644 --- a/addons/stock/models/__init__.py +++ b/addons/stock/models/__init__.py @@ -11,4 +11,5 @@ import stock_config_settings import stock_inventory import stock_picking import stock_quant +import stock_warehouse import web_planner diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py new file mode 100644 index 00000000000..763260046fc --- /dev/null +++ b/addons/stock/models/stock_warehouse.py @@ -0,0 +1,930 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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 + + +class stock_warehouse(osv.osv): + _name = "stock.warehouse" + _description = "Warehouse" + + _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"), + } + + 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) + + 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'] + + 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) + + 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 + + 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') + + 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 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 + + def _get_reception_delivery_route(self, cr, uid, ids, route_name, context=None): + warehouse = self.browse(cr, uid, ids, context=context)[0] + return { + 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), + '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: + raise UserError(_('Can\'t find any generic Make To Order route.')) + return mto_route_id + + 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] + return { + 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), + 'warehouse_selectable': False, + 'product_selectable': True, + 'product_categ_selectable': True, + 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', + 'sequence': 20, + } + + 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 + 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, + } + + 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 + + #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) + + 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) + + #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) + + route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context) + + #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 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) + + 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) + 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) + for mto_pull_val in mto_pull_vals: + pull_obj.create(cr, uid, mto_pull_val, context=context) + 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) + + 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')]) + if pulls: + pull_obj.write(cr, uid, pulls, {'location_id': new_location}, context=context) + + 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) + + 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)]}) + + # 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_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 + + 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() + + domain = [('id', 'in', all_routes)] + return { + 'name': _('Warehouse\'s Routes'), + 'domain': domain, + 'res_model': 'stock.location.route', + 'type': 'ir.actions.act_window', + 'view_id': False, + 'view_mode': 'tree,form', + 'view_type': 'form', + 'limit': 20 + } + + +class stock_warehouse_orderpoint(osv.osv): + """ + 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 + 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 = {} + + 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 + + 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 {} + + 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} + 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': + # These days will be substracted when creating the PO + days += orderpoint.product_id._select_seller(orderpoint.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) + return { + 'name': orderpoint.name, + 'date_planned': date or orderpoint._get_date_planned(datetime.today()), + 'product_id': orderpoint.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, + } diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 0389514cf28..4fed09628c3 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1404,771 +1404,10 @@ class stock_move(osv.osv): move.write({'partially_available': False}) self.pool['stock.quant'].write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False}, context=context) - +# WHERE IS ORDERPOINT ? #---------------------------------------------------------- # Stock Warehouse #---------------------------------------------------------- -class stock_warehouse(osv.osv): - _name = "stock.warehouse" - _description = "Warehouse" - - _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"), - } - - 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) - - 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'] - - 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) - - 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 - - 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') - - 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 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 - - def _get_reception_delivery_route(self, cr, uid, ids, route_name, context=None): - warehouse = self.browse(cr, uid, ids, context=context)[0] - return { - 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), - '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: - raise UserError(_('Can\'t find any generic Make To Order route.')) - return mto_route_id - - 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] - return { - 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), - 'warehouse_selectable': False, - 'product_selectable': True, - 'product_categ_selectable': True, - 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', - 'sequence': 20, - } - - 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 - 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, - } - - 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 - - #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) - - 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) - - #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) - - route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context) - - #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 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) - - 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) - 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) - for mto_pull_val in mto_pull_vals: - pull_obj.create(cr, uid, mto_pull_val, context=context) - 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) - - 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')]) - if pulls: - pull_obj.write(cr, uid, pulls, {'location_id': new_location}, context=context) - - 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) - - 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)]}) - - # 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_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 - - 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() - - domain = [('id', 'in', all_routes)] - return { - 'name': _('Warehouse\'s Routes'), - 'domain': domain, - 'res_model': 'stock.location.route', - 'type': 'ir.actions.act_window', - 'view_id': False, - 'view_mode': 'tree,form', - 'view_type': 'form', - 'limit': 20 - } - class stock_location_path(osv.osv): _name = "stock.location.path" @@ -2605,162 +1844,6 @@ class stock_move_operation_link(osv.osv): } -class stock_warehouse_orderpoint(osv.osv): - """ - 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 - 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 = {} - - 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 - - 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 {} - - 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} - 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': - # These days will be substracted when creating the PO - days += orderpoint.product_id._select_seller(orderpoint.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) - return { - 'name': orderpoint.name, - 'date_planned': date or orderpoint._get_date_planned(datetime.today()), - 'product_id': orderpoint.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, - } - - class StockPackOperation(models.Model): _inherit = 'stock.pack.operation' diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index 9cd147a310b..b1a99d19011 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -290,101 +290,6 @@ groups="stock.group_stock_multi_locations" parent="menu_warehouse_config" sequence="2"/> - - stock.warehouse - stock.warehouse - -
- -
- -
-
-
-
-
- - stock.warehouse.tree - stock.warehouse - - - - - - - - - - - stock.warehouse.search - stock.warehouse - - - - - - - - Warehouses - stock.warehouse - ir.actions.act_window - form - - -

- Click to define a new warehouse. -

-
-
- - - - Procurements - procurement.order - form - tree,form - [('orderpoint_id','=',active_id)] - - - - stock.warehouse.orderpoint.tree - stock.warehouse.orderpoint - - - - - - - - - - - - - - - - stock.warehouse.orderpoint.search - stock.warehouse.orderpoint - - - - - - - - - - - - - - - - - - stock.warehouse.orderpoint.form - stock.warehouse.orderpoint - -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
-
- - - Reordering Rules - stock.warehouse.orderpoint - ir.actions.act_window - form - - - -

- Click to add a reordering rule. -

You can define your minimum stock rules, so that Odoo will automatically create draft manufacturing orders or request for quotations according to the stock level. Once the virtual stock of a product (= stock on hand minus all confirmed orders and reservations) is below the minimum quantity, Odoo will generate a procurement request to increase the stock up to the maximum quantity.

-
-
- - - - {'search_default_route_false':1} Push Rules diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml new file mode 100644 index 00000000000..c3a6883482e --- /dev/null +++ b/addons/stock/views/stock_warehouse_views.xml @@ -0,0 +1,214 @@ + + + + + stock.warehouse + stock.warehouse + +
+ +
+ +
+
+
+
+
+ + stock.warehouse.tree + stock.warehouse + + + + + + + + + + + stock.warehouse.search + stock.warehouse + + + + + + + + Warehouses + stock.warehouse + ir.actions.act_window + form + + +

+ Click to define a new warehouse. +

+
+
+ + + + + Procurements + procurement.order + form + tree,form + [('orderpoint_id','=',active_id)] + + + + stock.warehouse.orderpoint.tree + stock.warehouse.orderpoint + + + + + + + + + + + + + + + + stock.warehouse.orderpoint.search + stock.warehouse.orderpoint + + + + + + + + + + + + + + + + + + stock.warehouse.orderpoint.form + stock.warehouse.orderpoint + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Reordering Rules + stock.warehouse.orderpoint + ir.actions.act_window + form + + + +

+ Click to add a reordering rule. +

You can define your minimum stock rules, so that Odoo will automatically create draft manufacturing orders or request for quotations according to the stock level. Once the virtual stock of a product (= stock on hand minus all confirmed orders and reservations) is below the minimum quantity, Odoo will generate a procurement request to increase the stock up to the maximum quantity.

+
+
+ + + + +
\ No newline at end of file