diff --git a/addons/sale_stock/res_config.py b/addons/sale_stock/res_config.py index 33cc95ca85d..58c4ead1c50 100644 --- a/addons/sale_stock/res_config.py +++ b/addons/sale_stock/res_config.py @@ -52,6 +52,9 @@ class sale_configuration(osv.osv_memory): help="Allows you to tag sales order lines with properties."), 'module_project_timesheet': fields.boolean("Project Timesheet"), 'module_project_mrp': fields.boolean("Project MRP"), + 'group_route_so_lines': fields.boolean('Choose MTO, Dropship, ... on sale order lines', + implied_group='sale_stock.group_route_so_lines', + help="Allows you to set route on sale order lines."), } _defaults = { diff --git a/addons/sale_stock/res_config_view.xml b/addons/sale_stock/res_config_view.xml index 93bc8e5980e..e7b32c0866d 100644 --- a/addons/sale_stock/res_config_view.xml +++ b/addons/sale_stock/res_config_view.xml @@ -1,7 +1,6 @@ - sale settings sale.config.settings @@ -68,9 +67,14 @@ + +
+ +
+
-
\ No newline at end of file diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 8ba711becb0..b36cc481a2b 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -89,6 +89,11 @@ class sale_order(osv.osv): vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context) location_id = order.partner_shipping_id.property_stock_customer.id vals['location_id'] = location_id + + routes = [] + routes += order.warehouse_id and [(4, x.id) for x in order.warehouse_id.route_ids] or [] #route_ids + routes += line.route_id and [(4, line.route_id.id)] or [] #route_id + vals['route_ids'] = routes return vals _columns = { @@ -251,6 +256,7 @@ class sale_order_line(osv.osv): _columns = { 'product_packaging': fields.many2one('product.packaging', 'Packaging'), 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'), + 'route_id': fields.many2one('stock.location.route', 'Route', domain=[('sale_selectable', '=', True)]), } _defaults = { @@ -431,3 +437,10 @@ class stock_move(osv.osv): res['price_unit'] = sale_line.price_unit res['discount'] = sale_line.discount return res + + +class stock_location_route(osv.osv): + _inherit = "stock.location.route" + _columns = { + 'sale_selectable':fields.boolean("Selectable on Sales Order Line") + } diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml index efc3d40adde..448df25c446 100644 --- a/addons/sale_stock/sale_stock_view.xml +++ b/addons/sale_stock/sale_stock_view.xml @@ -71,6 +71,12 @@ + + + + + + @@ -115,5 +121,28 @@ + + sale.order.line.form.sale.stock.location + sale.order.line + + + + + + + + + + + + sale.order.line.tree.sale.stock.location + + sale.order.line + + + + + + diff --git a/addons/sale_stock/security/sale_stock_security.xml b/addons/sale_stock/security/sale_stock_security.xml index 7f93084d43d..0f70053cd7c 100644 --- a/addons/sale_stock/security/sale_stock_security.xml +++ b/addons/sale_stock/security/sale_stock_security.xml @@ -1,11 +1,14 @@ - - - - Enable Invoicing Delivery orders - - - + + + Enable Invoicing Delivery orders + + + + + Enable Route on Sales Order Line + + diff --git a/addons/sale_stock/stock_view.xml b/addons/sale_stock/stock_view.xml index 9091787a7af..0f5898e3016 100644 --- a/addons/sale_stock/stock_view.xml +++ b/addons/sale_stock/stock_view.xml @@ -15,6 +15,32 @@ + + + + + + + stock.location.route.form + + stock.location.route + + + + + + + + stock.location.route.tree.inherit + + stock.location.route + + + + + + + diff --git a/addons/sale_stock_location/__init__.py b/addons/sale_stock_location/__init__.py deleted file mode 100644 index 488ab65e79f..00000000000 --- a/addons/sale_stock_location/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import sale_stock_location -import res_config - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/sale_stock_location/__openerp__.py b/addons/sale_stock_location/__openerp__.py deleted file mode 100644 index 360994a9b9a..00000000000 --- a/addons/sale_stock_location/__openerp__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -{ - 'name': 'Sales and Warehouse and Route Management', - 'version': '1.0', - 'category': 'Hidden', - 'summary': 'Quotation, Sale Orders, Delivery & Invoicing Control', - 'description': """ -Manage sales quotations and stock_location -========================================== - -This adds a route on the sales order and sales order line (mini module) - -""", - 'author': 'OpenERP SA', - 'website': 'http://www.openerp.com', - 'images': [], - 'depends': ['sale', 'stock_location'], - 'init_xml': [], - 'update_xml': ['sale_stock_location_view.xml', - 'security/sale_stock_location_security.xml', - 'res_config_view.xml'], - 'demo_xml': [], - 'test': [], - 'installable': True, - 'auto_install': True, -} -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_stock_location/res_config.py b/addons/sale_stock_location/res_config.py deleted file mode 100644 index dc2e0f3df8f..00000000000 --- a/addons/sale_stock_location/res_config.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Business Applications -# Copyright (C) 2004-2012 OpenERP S.A. (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp.osv import fields, osv -from openerp.tools.translate import _ - -class sale_configuration(osv.osv_memory): - _inherit = 'sale.config.settings' - - _columns = { - 'group_route_so_lines': fields.boolean('Choose MTO, Dropship, ... on sale order lines', - implied_group='sale_stock_location.group_route_so_lines', - help="Allows you to set route on sale order lines."), - } - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_stock_location/res_config_view.xml b/addons/sale_stock_location/res_config_view.xml deleted file mode 100644 index 93fc93ac27c..00000000000 --- a/addons/sale_stock_location/res_config_view.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - sale stock location settings - sale.config.settings - - - - -
- -
-
-
-
-
- -
-
\ No newline at end of file diff --git a/addons/sale_stock_location/sale_stock_location.py b/addons/sale_stock_location/sale_stock_location.py deleted file mode 100644 index ac08fd21547..00000000000 --- a/addons/sale_stock_location/sale_stock_location.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, - -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from datetime import datetime, timedelta -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare -from dateutil.relativedelta import relativedelta -from openerp.osv import fields, osv -from openerp.tools.translate import _ - - -class sale_order(osv.osv): - _inherit = "sale.order" - - - def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): - ''' - Add route_ids to the procurement. As such, people should choose between them. - ''' - res = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context) - routes = [] - route_ids = order.warehouse_id and [(4, x.id) for x in order.warehouse_id.route_ids] or [] - routes += route_ids - route_id = line.route_id and [(4, line.route_id.id)] or [] - routes += route_id - print 'toutes les routes sur le procurement', routes - res.update({ - 'route_ids': routes - }) - return res - -class sale_order_line(osv.osv): - _inherit = 'sale.order.line' - _columns = { - 'route_id': fields.many2one('stock.location.route', 'Route', domain=[('sale_selectable', '=', True)]), - } - - -class stock_location_route(osv.osv): - _inherit = "stock.location.route" - _columns = { - 'sale_selectable':fields.boolean("Selectable on Sales Order Line") - } - - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale_stock_location/sale_stock_location_view.xml b/addons/sale_stock_location/sale_stock_location_view.xml deleted file mode 100644 index 63503ba1472..00000000000 --- a/addons/sale_stock_location/sale_stock_location_view.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - sale.order.line.form.sale.stock.location - sale.order.line - - - - - - - - - - - - sale.order.line.tree.sale.stock.location - - sale.order.line - - - - - - - - sale.order.form - - sale.order - - - - - - - - - - - - stock.location.route.form - - stock.location.route - - - - - - - - stock.location.route.tree.inherit - - stock.location.route - - - - - - - - - diff --git a/addons/sale_stock_location/security/sale_stock_location_security.xml b/addons/sale_stock_location/security/sale_stock_location_security.xml deleted file mode 100644 index 4114242054c..00000000000 --- a/addons/sale_stock_location/security/sale_stock_location_security.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Enable Route on Sales Order Line - - - - - diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 3bb0804c825..998810b42d6 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -58,15 +58,20 @@ Dashboard / Reports for Warehouse Management will include: 'category': 'Warehouse Management', 'sequence': 16, 'demo': [ + 'stock_demo_pre.yml', 'stock_demo.xml', 'procurement_demo.xml', 'stock_orderpoint.xml', + 'stock_orderpoint.yml', 'stock_demo.yml', + 'stock_location_demo_cpu1.xml', + 'stock_location_demo_cpu3.yml', ], 'data': [ 'security/stock_security.xml', 'security/ir.model.access.csv', 'stock_data.xml', + 'stock_data.yml', 'wizard/stock_move_view.xml', 'wizard/stock_change_product_qty_view.xml', 'wizard/stock_inventory_merge_view.xml', diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py index 1053eca3d54..c1866867894 100644 --- a/addons/stock/procurement.py +++ b/addons/stock/procurement.py @@ -79,12 +79,18 @@ class procurement_rule(osv.osv): 'stock.location.route': (_get_route, ['active'], 20), 'procurement.rule': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),}, help="If the active field is set to False, it will allow you to hide the rule without removing it." ), + 'delay': fields.integer('Number of Days'), + 'partner_address_id': fields.many2one('res.partner', 'Partner Address'), + 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move of the move (which was generated by a next procurement) is cancelled or split, the move generated by this move will too'), + } _defaults = { 'procure_method': 'make_to_stock', 'sequence': 20, 'active': True, + 'propagate': True, + 'delay': 0, } class procurement_order(osv.osv): @@ -93,26 +99,40 @@ class procurement_order(osv.osv): 'location_id': fields.many2one('stock.location', 'Procurement Location'), # not required because task may create procurements that aren't linked to a location with project_mrp 'move_ids': fields.one2many('stock.move', 'procurement_id', 'Moves', help="Moves created by the procurement"), 'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Move which caused (created) the procurement"), + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_procurement', 'procurement_id', 'route_id', 'Followed Route', help="Preferred route to be followed by the procurement order"), + } + + def _find_parent_locations(self, cr, uid, procurement, context=None): + location = procurement.location_id + res = [location.id] + while location.location_id: + location = location.location_id + res.append(location.id) + return res def _search_suitable_rule(self, cr, uid, procurement, domain, context=None): '''we try to first find a rule among the ones defined on the procurement order group and if none is found, we try on the routes defined for the product, and finally we fallback on the default behavior''' - route_ids = [x.id for x in procurement.product_id.route_ids] - res = self.pool.get('procurement.rule').search(cr, uid, domain + [('route_id', 'in', route_ids)], order = 'route_sequence, sequence', context=context) + product_route_ids = [x.id for x in procurement.product_id.route_ids + procurement.product_id.categ_id.total_route_ids] + procurement_route_ids = [x.id for x in procurement.route_ids] + res = self.pool.get('procurement.rule').search(cr, uid, domain + [('route_id', 'in', product_route_ids)], order = 'route_sequence, sequence', context=context) if not res: - res = self.pool.get('procurement.rule').search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context) + res = self.pool.get('procurement.rule').search(cr, uid, domain + [('route_id', 'in', procurement_route_ids)], order = 'route_sequence, sequence', context=context) + if not res: + res = self.pool.get('procurement.rule').search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context) return res - def _find_suitable_rule(self, cr, uid, procurement, context=None): rule_id = super(procurement_order, self)._find_suitable_rule(cr, uid, procurement, context=context) if not rule_id: - rule_id = self._search_suitable_rule(cr, uid, procurement, [('location_id', 'child_of', procurement.location_id.id)], context=context) #action=move + #a rule defined on 'Stock' is suitable for a procurement in 'Stock\Bin A' + all_parent_location_ids = self._find_parent_locations(cr, uid, procurement, context=context) + rule_id = self._search_suitable_rule(cr, uid, procurement, [('location_id', 'in', all_parent_location_ids)], context=context) rule_id = rule_id and rule_id[0] or False return rule_id def _run_move_create(self, cr, uid, procurement, context=None): - return { + vals = { 'name': procurement.name, 'company_id': procurement.company_id.id, 'product_id': procurement.product_id.id, @@ -136,7 +156,15 @@ class procurement_order(osv.osv): 'origin': procurement.origin, 'picking_type_id': procurement.rule_id.picking_type_id.id, 'group_id': procurement.group_id and procurement.group_id.id or False, - } + 'route_ids': [(4, x.id) for x in procurement.route_ids], + } + if procurement.rule_id: + newdate = (datetime.strptime(procurement.date_planned, '%Y-%m-%d %H:%M:%S') - relativedelta(days=procurement.rule_id.delay or 0)).strftime('%Y-%m-%d %H:%M:%S') + vals.update({ + 'date': newdate, + 'propagate': procurement.rule_id.propagate, + }) + return vals def _run(self, cr, uid, procurement, context=None): if procurement.rule_id and procurement.rule_id.action == 'move': diff --git a/addons/stock/product.py b/addons/stock/product.py index 9e436dd49c3..4883497e77c 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -134,11 +134,10 @@ class product_product(osv.osv): # if field_names in ['incoming_qty', 'outgoing_qty', 'virtual_available']: moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context) moves_out = self.pool.get('stock.move').read_group(cr, uid, domain_move_out, ['product_id', 'product_qty'], ['product_id'], context=context) - + quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context) - quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants)) - + moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in)) moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out)) @@ -149,7 +148,8 @@ class product_product(osv.osv): 'incoming_qty': moves_in.get(id, 0.0), 'outgoing_qty': moves_out.get(id, 0.0), 'virtual_available': quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0), - } + } + return res _columns = { @@ -216,7 +216,7 @@ class product_product(osv.osv): 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'), 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'), 'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'), - 'route_ids': fields.many2many('stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes', + 'route_ids': fields.many2many('stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes', domain="[('product_selectable', '=', True)]", help="Depending on the modules installed, this will allow you to define the route of the product: whether it will be bought, manufactured, MTO/MTS,..."), } @@ -263,6 +263,20 @@ class product_product(osv.osv): res['fields']['qty_available']['string'] = _('Produced Qty') return res + def action_view_routes(self, cr, uid, ids, context=None): + route_obj = self.pool.get("stock.location.route") + act_obj = self.pool.get('ir.actions.act_window') + mod_obj = self.pool.get('ir.model.data') + product_route_ids = set() + for product in self.browse(cr, uid, ids, context=context): + product_route_ids |= set([r.id for r in product.route_ids]) + product_route_ids |= set([r.id for r in product.categ_id.total_route_ids]) + route_ids = route_obj.search(cr, uid, ['|', ('id', 'in', list(product_route_ids)), ('warehouse_selectable', '=', True)], context=context) + result = mod_obj.get_object_reference(cr, uid, 'stock_location', 'action_routes_form') + id = result and result[1] or False + result = act_obj.read(cr, uid, [id], context=context)[0] + result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]" + return result class product_template(osv.osv): _name = 'product.template' @@ -296,6 +310,52 @@ class product_template(osv.osv): _defaults = { 'sale_delay': 7, } + + +class product_removal_strategy(osv.osv): + _name = 'product.removal' + _description = 'Removal Strategy' + _order = 'sequence' + _columns = { + 'product_categ_id': fields.many2one('product.category', 'Category', required=True), + 'sequence': fields.integer('Sequence'), + 'method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO')], "Method", required = True), + 'location_id': fields.many2one('stock.location', 'Locations', required=True), + } + + +class product_putaway_strategy(osv.osv): + _name = 'product.putaway' + _description = 'Put Away Strategy' + _columns = { + 'product_categ_id':fields.many2one('product.category', 'Product Category', required=True), + 'location_id': fields.many2one('stock.location','Parent Location', help="Parent Destination Location from which a child bin location needs to be chosen", required=True), #domain=[('type', '=', 'parent')], + 'method': fields.selection([('fixed', 'Fixed Location')], "Method", required = True), + 'location_spec_id': fields.many2one('stock.location','Specific Location', help="When the location is specific, it will be put over there"), #domain=[('type', '=', 'parent')], + } + + +class product_category(osv.osv): + _inherit = 'product.category' + + def calculate_total_routes(self, cr, uid, ids, name, args, context=None): + res = {} + route_obj = self.pool.get("stock.location.route") + for categ in self.browse(cr, uid, ids, context=context): + categ2 = categ + routes = [x.id for x in categ.route_ids] + while categ2.parent_id: + categ2 = categ2.parent_id + routes += [x.id for x in categ2.route_ids] + res[categ.id] = routes + return res + + _columns = { + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"), + 'removal_strategy_ids': fields.one2many('product.removal', 'product_categ_id', 'Removal Strategies'), + 'putaway_strategy_ids': fields.one2many('product.putaway', 'product_categ_id', 'Put Away Strategies'), + 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True), + } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml index eb7193b210c..acc271bd383 100644 --- a/addons/stock/product_view.xml +++ b/addons/stock/product_view.xml @@ -51,7 +51,7 @@ ir.actions.act_window form tree,form - + @@ -72,7 +72,7 @@ form tree,form - +

diff --git a/addons/stock/res_config.py b/addons/stock/res_config.py index 60fa04dea2c..eb5218f160f 100644 --- a/addons/stock/res_config.py +++ b/addons/stock/res_config.py @@ -46,11 +46,6 @@ The following dates can be tracked: - removal date - alert date. This installs the module product_expiry."""), - 'module_stock_location': fields.boolean("Create push/pull logistic rules", - help='Provide push and pull inventory flows. Typical uses of this feature are: ' - 'manage product manufacturing chains, manage default locations per product, ' - 'define routes within your warehouse according to business needs, etc.\n' - '-This installs the module stock_location.'), 'group_uom': fields.boolean("Manage different units of measure for products", implied_group='product.group_uom', help="""Allows you to select and maintain different units of measure for products."""), @@ -74,8 +69,11 @@ This installs the module product_expiry."""), help="""Allows to configure inventory valuations on products and product categories."""), 'group_stock_multiple_locations': fields.boolean("Manage multiple locations and warehouses", implied_group='stock.group_locations', - help='This allows to configure and use multiple stock locations and warehouses, ' - 'instead of having a single default one.'), + help="""This allows to configure and use multiple stock locations and warehouses, + instead of having a single default one."""), + 'group_stock_adv_location': fields.boolean("Active Push and Pull inventory flows", + implied_group='stock.group_adv_location', + help="""This option supplements the warehouse application by effectively implementing Push and Pull inventory flows. """), 'decimal_precision': fields.integer('Decimal precision on weight', help="As an example, a decimal precision of 2 will allow weights like: 9.99 kg, whereas a decimal precision of 4 will allow weights like: 0.0231 kg."), } diff --git a/addons/stock/res_config_view.xml b/addons/stock/res_config_view.xml index 471d1035ad1..befc46c1cf4 100644 --- a/addons/stock/res_config_view.xml +++ b/addons/stock/res_config_view.xml @@ -70,13 +70,19 @@

+
+ +
diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv index 03690fe6cda..e30e3c96672 100644 --- a/addons/stock/security/ir.model.access.csv +++ b/addons/stock/security/ir.model.access.csv @@ -45,9 +45,24 @@ access_product_pricelist_item_stock_manager,product.pricelist.item stock_manager access_board_stock_user,board.board user,board.model_board_board,stock.group_stock_user,1,1,0,0 access_stock_warehouse_orderpoint,stock.warehouse.orderpoint,model_stock_warehouse_orderpoint,stock.group_stock_user,1,0,0,0 access_stock_warehouse_orderpoint_system,stock.warehouse.orderpoint system,model_stock_warehouse_orderpoint,stock.group_stock_manager,1,1,1,1 -access_stock_quant_manager,stock.quant manager,model_stock_quant,stock.group_stock_manager,1,1,0,0 -access_stock_quant_user,stock.quant user,model_stock_quant,stock.group_stock_user,1,1,0,0 +access_stock_quant_manager,stock.quant manager,model_stock_quant,stock.group_stock_manager,1,0,0,0 +access_stock_quant_user,stock.quant user,model_stock_quant,stock.group_stock_user,1,0,0,0 access_stock_quant_all,stock.quant all users,model_stock_quant,base.group_user,1,0,0,0 +access_procurement_rule_user,procurement_rule user,model_procurement_rule,stock.group_stock_user,1,0,0,0 +access_procurement_rule_manager,procurement_rule manager,model_procurement_rule,stock.group_stock_manager,1,1,1,1 +access_procurement_rule_salemanager,procurement_rule salemanager,model_procurement_rule,base.group_sale_manager,1,1,1,1 +access_procurement_rule_stock_manager,procurement_rule stock manager,model_procurement_rule,stock.group_stock_manager,1,1,1,1 +access_stock_location_path_user,stock location path user,model_stock_location_path,stock.group_stock_user,1,0,0,0 +access_stock_location_path_internal_user,stock location path internal user,model_stock_location_path,base.group_user,1,0,0,0 +access_stock_location_path_sale_manager,stock.location.path partner salemanager,model_stock_location_path,base.group_sale_manager,1,1,1,1 +access_stock_location_path_stock_user,stock.location.path stock user,model_stock_location_path,stock.group_stock_user,1,1,1,1 +access_stock_location_path,stock.location.path,model_stock_location_path,base.group_sale_salesman,1,0,0,0 +access_stock_location_route,stock.location.route,model_stock_location_route,stock.group_stock_manager,1,0,1,0 +access_procurement_rule,procurement.rule.flow,model_procurement_rule,base.group_sale_salesman,1,0,0,0 +access_procurement_rule_internal,procurement.rule.flow internal,model_procurement_rule,base.group_user,1,0,0,0 access_stock_pack_operation_manager,stock.pack.operation manager,model_stock_pack_operation,stock.group_stock_manager,1,1,1,1 access_stock_pack_operation_user,stock.pack.operation user,model_stock_pack_operation,stock.group_stock_user,1,1,1,1 access_stock_pack_operation_all,stock.pack.operation all users,model_stock_pack_operation,base.group_user,1,0,0,0 +access_stock_putaway_all,stock.putaway all users,model_stock_move_putaway,base.group_user,1,0,0,0 +access_product_putaway_all,product.putaway all users,model_product_putaway,base.group_user,1,0,0,0 +access_stock_removal_all,product.removal all users,model_product_removal,base.group_user,1,0,0,0 diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml index 105c4a7b7ce..5f8d4a391a4 100644 --- a/addons/stock/security/stock_security.xml +++ b/addons/stock/security/stock_security.xml @@ -29,6 +29,12 @@ + + Manage Push and Pull inventory flows + + + + Manage Different Stock Owners @@ -87,6 +93,19 @@ ['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)] + + product_pulled_flow multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + stock_location_path multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 419c0ffb5bb..44301a5aace 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -115,6 +115,8 @@ class stock_location(osv.osv): 'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between all companies'), 'scrap_location': fields.boolean('Scrap Location', help='Check this box to allow using this location to put scrapped/damaged goods.'), + 'removal_strategy_ids': fields.one2many('product.removal', 'location_id', 'Removal Strategies'), + 'putaway_strategy_ids': fields.one2many('product.putaway', 'location_id', 'Put Away Strategies'), } _defaults = { 'active': True, @@ -125,8 +127,36 @@ class stock_location(osv.osv): 'posz': 0, 'scrap_location': False, } + + def get_putaway_strategy(self, cr, uid, location, product, context=None): + pa = self.pool.get('product.putaway') + categ = product.categ_id + categs = [categ.id, False] + while categ.parent_id: + categ = categ.parent_id + categs.append(categ.id) + + result = pa.search(cr,uid, [ + ('location_id', '=', location.id), + ('product_categ_id', 'in', categs) + ], context=context) + if result: + return pa.browse(cr, uid, result[0], context=context) + def get_removal_strategy(self, cr, uid, location, product, context=None): - return None + pr = self.pool.get('product.removal') + categ = product.categ_id + categs = [categ.id, False] + while categ.parent_id: + categ = categ.parent_id + categs.append(categ.id) + + result = pr.search(cr,uid, [ + ('location_id', '=', location.id), + ('product_categ_id', 'in', categs) + ], context=context) + if result: + return pr.browse(cr, uid, result[0], context=context).method #---------------------------------------------------------- @@ -142,12 +172,19 @@ class stock_location_route(osv.osv): 'name': fields.char('Route Name', required=True), 'sequence': fields.integer('Sequence'), 'pull_ids': fields.one2many('procurement.rule', 'route_id', 'Pull Rules'), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it.") + 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it."), + 'push_ids': fields.one2many('stock.location.path', 'route_id', 'Push Rules'), + 'product_selectable': fields.boolean('Selectable on Product'), + 'product_categ_selectable': fields.boolean('Selectable on Product Category'), + 'warehouse_selectable': fields.boolean('Selectable on Warehouse'), + 'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'), + 'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'), } _defaults = { 'sequence': lambda self, cr, uid, ctx: 0, 'active': True, + 'product_selectable': True, } @@ -222,6 +259,9 @@ class stock_quant(osv.osv): def check_preferred_location(self, cr, uid, move, context=None): + if move.putaway_ids and move.putaway_ids[0]: + #Take only first suggestion for the moment + return move.putaway_ids[0].location_id return move.location_dest_id def move_single_quant(self, cr, uid, quant, qty, move, lot_id = False, owner_id = False, package_id = False, context=None): @@ -1318,7 +1358,9 @@ class stock_move(osv.osv): 'availability': fields.function(_get_product_availability, type='float', string='Availability'), 'restrict_lot_id': fields.many2one('stock.production.lot', 'Lot', help="Technical field used to depict a restriction on the lot of quants to consider when marking this move as 'done'"), 'restrict_partner_id': fields.many2one('res.partner', 'Owner ', help="Technical field used to depict a restriction on the ownership of quants to consider when marking this move as 'done'"), - } + 'putaway_ids': fields.one2many('stock.move.putaway', 'move_id', 'Put Away Suggestions'), + 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order"), + } def copy(self, cr, uid, id, default=None, context=None): if default is None: @@ -1368,7 +1410,7 @@ class stock_move(osv.osv): def _prepare_procurement_from_move(self, cr, uid, move, context=None): origin = (move.group_id and (move.group_id.name+":") or "") + (move.rule_id and move.rule_id.name or "/") - return { + return { 'name': move.rule_id and move.rule_id.name or "/", 'origin': origin, 'company_id': move.company_id and move.company_id.id or False, @@ -1381,8 +1423,34 @@ class stock_move(osv.osv): 'location_id': move.location_id.id, 'move_dest_id': move.id, 'group_id': move.group_id and move.group_id.id or False, + 'route_ids' : [(4, x.id) for x in move.route_ids], } + def _push_apply(self, cr, uid, moves, context): + push_obj = self.pool.get("stock.location.path") + for move in moves: + if not move.move_dest_id: + routes = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids] + rules = push_obj.search(cr, uid, [('route_id', 'in', routes), ('location_from_id', '=', move.location_dest_id.id)], context=context) + if rules: + rule = push_obj.browse(cr, uid, rules[0], context=context) + push_obj._apply(cr, uid, rule, move, context=context) + return True + + # Create the stock.move.putaway records + def _putaway_apply(self,cr, uid, ids, context=None): + moveputaway_obj = self.pool.get('stock.move.putaway') + for move in self.browse(cr, uid, ids, context=context): + putaway = self.pool.get('stock.location').get_putaway_strategy(cr, uid, move.location_dest_id, move.product_id, context=context) + if putaway: + # Should call different methods here in later versions + # TODO: take care of lots + if putaway.method == 'fixed' and putaway.location_spec_id: + moveputaway_obj.create(cr, SUPERUSER_ID, {'move_id': move.id, + 'location_id': putaway.location_spec_id.id, + 'quantity': move.product_uom_qty}, context=context) + return True + def _create_procurement(self, cr, uid, move, context=None): """ This will create a procurement order @@ -1574,6 +1642,8 @@ class stock_move(osv.osv): for move in self.browse(cr, uid, write_ids, context=context): if move.procure_method == 'make_to_order': self._create_procurement(cr, uid, move, context=context) + moves = self.browse(cr, uid, ids, context=context) + self._push_apply(cr, uid, moves, context=context) return True def force_assign(self, cr, uid, ids, context=None): @@ -1621,6 +1691,7 @@ class stock_move(osv.osv): if all(map(lambda x:x[0], quants)): done.append(move.id) self.write(cr, uid, done, {'state': 'assigned'}) + self._putaway_apply(cr, uid, ids, context=context) return done @@ -1908,8 +1979,8 @@ class stock_inventory(osv.osv): def _default_stock_location(self, cr, uid, context=None): try: - stock_location = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock') - return stock_location.id + warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0') + return warehouse.lot_stock_id.id except: return False @@ -2152,21 +2223,635 @@ class stock_warehouse(osv.osv): 'partner_id': fields.many2one('res.partner', 'Address'), 'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage', '=', 'internal')]), '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', 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', 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', 'Reception Route'), + 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'), + 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'), + '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'), + 'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse'), } + def _get_inter_wh_location(self, cr, uid, warehouse, 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') + try: + inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1] + except: + inter_wh_loc = False + return inter_wh_loc + + def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None): + return self.pool.get('product.product').search(cr, uid, [], context=context) + + def _assign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None): + product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context) + self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(4, inter_wh_route_id)]}, context=context) + + def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None): + 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, warehouse, supplier_warehouses, default_resupply_wh, context=None): + location_obj = self.pool.get('stock.location') + 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 + inter_wh_location_id = self._get_inter_wh_location(cr, uid, warehouse, context=context) + if inter_wh_location_id: + input_loc = warehouse.wh_input_stock_loc_id + if warehouse.reception_steps == 'one_step': + input_loc = warehouse.lot_stock_id + inter_wh_location = location_obj.browse(cr, uid, inter_wh_location_id, context=context) + for wh in supplier_warehouses: + output_loc = wh.wh_output_stock_loc_id + if wh.delivery_steps == 'ship_only': + output_loc = wh.lot_stock_id + inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context) + inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context) + values = [(output_loc, inter_wh_location, wh.out_type_id.id), (inter_wh_location, input_loc, warehouse.in_type_id.id)] + dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, 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 all product + if default_resupply_wh and default_resupply_wh.id == wh.id: + self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context) + def _default_stock_id(self, cr, uid, context=None): - lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock') - return lot_input_stock.id + #lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock') + try: + warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0') + return warehouse.lot_stock_id.id + except: + return False _defaults = { 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c), 'lot_stock_id': _default_stock_id, + '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 osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.')) + return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context) + + def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None): + 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: + 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: + location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, 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, warehouse, route_name, context=None): + return { + 'name': self._format_routename(cr, uid, warehouse, route_name, context=context), + 'product_categ_selectable': True, + 'product_selectable': False, + 'sequence': 10, + } + + def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None): + 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, + }) + 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, + }) + first_rule = False + return push_rules_list, pull_rules_list + + def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None): + 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', _('MTO'))], context=context) + mto_route_id = mto_route_id and mto_route_id[0] or False + if not mto_route_id: + raise osv.except_osv(_('Error!'), _('Can\'t find any generic MTO route.')) + + from_loc, dest_loc, pick_type_id = values[0] + return { + '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, + } + + def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None): + 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', + } + + def create_routes(self, cr, uid, ids, warehouse, context=None): + 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 = self.get_routes_dict(cr, uid, ids, warehouse, context=context) + #create reception route and rules + route_name, values = routes_dict[warehouse.reception_steps] + route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context) + 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 = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context) + #create the push/pull 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: + pull_obj.create(cr, uid, vals=pull_rule, context=context) + + #create MTS route and pull rules for delivery a specific route MTO to be set on the product + route_name, values = routes_dict[warehouse.delivery_steps] + route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context) + #create the route and its pull 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 = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context) + for pull_rule in pull_rules_list: + pull_obj.create(cr, uid, vals=pull_rule, context=context) + #create MTO pull rule and link it to the generic MTO route + mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context) + 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 = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context) + 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 = self._get_push_pull_rules(cr, uid, warehouse, warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', values, crossdock_route_id, context=context) + for pull_rule in pull_rules_list: + pull_obj.create(cr, uid, vals=pull_rule, context=context) + + #create route selectable on the product to resupply the warehouse from another one + self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context) + + #set routes and mto pull rule on warehouse + return self.write(cr, uid, warehouse.id, { + '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, + }, context=context) + + def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None): + 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.int_type_id.id, {'active': new_reception_step != 'one_step'}, context=context) + picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context) + picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context) + + routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context) + #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 = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context) + #create the pull rules + for pull_rule in pull_rules_list: + pull_obj.create(cr, uid, vals=pull_rule, context=context) + + #update reception route and rules: unlink the existing rules of the warehouse reception 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 = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context) + #create the push/pull 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: + 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 = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context) + pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context) + return True + + 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 + wh_loc_id = location_obj.create(cr, uid, { + 'name': _(vals.get('name')), + 'usage': 'view', + 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1] + }, context=context) + #create all location + reception_steps = vals.get('reception_steps', False) + delivery_steps = vals.get('delivery_steps', False) + 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: + location_id = location_obj.create(cr, uid, { + 'name': values['name'], + 'usage': 'internal', + 'location_id': wh_loc_id, + 'active': values['active'], + }, context=context_with_inactive) + vals[values['field']] = location_id + + #create new sequences + in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence in'), 'prefix': vals.get('code', '') + '\IN\\', 'padding': 5}, context=context) + out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence out'), 'prefix': vals.get('code', '') + '\OUT\\', 'padding': 5}, context=context) + pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence packing'), 'prefix': vals.get('code', '') + '\PACK\\', 'padding': 5}, context=context) + pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence picking'), 'prefix': vals.get('code', '') + '\PICK\\', 'padding': 5}, context=context) + int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence internal'), 'prefix': vals.get('code', '') + '\INT\\', 'padding': 5}, context=context) + + #create WH + new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context) + + warehouse = self.browse(cr, uid, new_id, 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 + + #fetch customer and supplier locations, for references + customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context) + + #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 + all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color') + not_used_colors = list(set(range(1, 10)) - set([x['color'] for x in all_used_colors])) + color = not_used_colors and not_used_colors[0] or 1 + + in_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Receptions'), + 'warehouse_id': new_id, + 'code_id': 'incoming', + 'auto_force_assign': True, + 'sequence_id': in_seq_id, + 'default_location_src_id': supplier_loc.id, + 'default_location_dest_id': input_loc.id, + 'color': color}, context=context) + out_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Delivery Orders'), + 'warehouse_id': new_id, + 'code_id': 'outgoing', + 'sequence_id': out_seq_id, + 'delivery': True, + 'default_location_src_id': output_loc.id, + 'default_location_dest_id': customer_loc.id, + 'color': color}, context=context) + int_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Internal Transfers'), + 'warehouse_id': new_id, + 'code_id': 'internal', + 'sequence_id': int_seq_id, + 'default_location_src_id': wh_stock_loc.id, + 'default_location_dest_id': wh_stock_loc.id, + 'active': reception_steps != 'one_step', + 'pack': False, + 'color': color}, context=context) + pack_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Pack'), + 'warehouse_id': new_id, + 'code_id': 'internal', + 'sequence_id': pack_seq_id, + 'default_location_src_id': wh_pack_stock_loc.id, + 'default_location_dest_id': output_loc.id, + 'active': delivery_steps == 'pick_pack_ship', + 'pack': True, + 'color': color}, context=context) + pick_type_id = picking_type_obj.create(cr, uid, vals={ + 'name': _('Pick'), + 'warehouse_id': new_id, + 'code_id': 'internal', + 'sequence_id': pick_seq_id, + 'default_location_src_id': wh_stock_loc.id, + 'default_location_dest_id': wh_pack_stock_loc.id, + 'active': delivery_steps != 'ship_only', + 'pack': False, + '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, new_id, vals=vals, context=context) + warehouse.refresh() + + #create routes and push/pull rules + self.create_routes(cr, uid, new_id, warehouse, context=context) + return new_id + + def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None): + return obj.name + ': ' + 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, warehouse, context=None): + #fetch customer and supplier locations, for references + customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context) + + return { + 'one_step': (_('Reception in 1 step'), []), + 'two_steps': (_('Reception in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]), + 'three_steps': (_('Reception 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.int_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 write(self, cr, uid, ids, vals, context=None): + if context is None: + context = {} + if isinstance(ids, (int, long)): + ids = [ids] + if context is None: + context = {} + seq_obj = self.pool.get('ir.sequence') + location_obj = self.pool.get('stock.location') + route_obj = self.pool.get('stock.location.route') + warehouse_obj = self.pool.get('stock.warehouse') + pull_obj = self.pool.get('procurement.rule') + push_obj = self.pool.get('stock.location.path') + + 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 + self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context) + # switch between route + self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive) + if vals.get('code') or vals.get('name'): + name = warehouse.name + #rename sequence + if vals.get('name'): + name = vals.get('name') + #rename location + location_id = warehouse.lot_stock_id.location_id.id + location_obj.write(cr, uid, location_id, {'name': name}, context=context_with_inactive) + #rename route and push-pull rules + for route in warehouse.route_ids: + route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context_with_inactive) + for pull in route.pull_ids: + pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context_with_inactive) + for push in route.push_ids: + push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context_with_inactive) + #change the mto pull rule name + pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context_with_inactive) + 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) + 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) + 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) + 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) + 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 + supplier_warehouses = warehouse_obj.browse(cr, uid, list(to_add_wh_ids), context=context) + self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context) + to_remove_wh_ids = old_ids - new_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) + route_obj.unlink(cr, uid, to_remove_route_ids, context=context) + else: + #not implemented + pass + if 'default_resupply_wh_id' in vals: + if warehouse.default_resupply_wh_id: + 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) + route_obj.unlink(cr, uid, to_remove_route_ids, context=context) + self._create_resupply_routes(cr, uid, warehouse, [warehouse.default_resupply_wh_id], False, context=context) + if vals.get('default_resupply_wh_id'): + to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', vals.get('default_resupply_wh_id'))], context=context) + route_obj.unlink(cr, uid, to_remove_route_ids, context=context) + def_supplier_wh = warehouse_obj.browse(cr, uid, vals['default_resupply_wh_id'], context=context) + self._create_resupply_routes(cr, uid, warehouse, [def_supplier_wh], def_supplier_wh, context=context) + + return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context) + + def unlink(self, cr, uid, ids, context=None): + #TODO try to delete location and route and if not possible, put them in inactive + return super(stock_warehouse, self).unlink(cr, uid, ids, context=context) + + +class stock_location_path(osv.osv): + _name = "stock.location.path" + _description = "Pushed Flows" + _order = "name" + + def _get_route(self, cr, uid, ids, context=None): + #WARNING TODO route_id is not required, so a field related seems a bad idea >-< + if context is None: + context = {} + result = {} + if context is None: + context = {} + context_with_inactive = context.copy() + context_with_inactive['active_test']=False + for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive): + for push_rule in route.push_ids: + result[push_rule.id] = True + return result.keys() + + _columns = { + 'name': fields.char('Operation Name', size=64, required=True), + 'company_id': fields.many2one('res.company', 'Company'), + 'route_id': fields.many2one('stock.location.route', 'Route'), + 'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True), + 'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True), + 'delay': fields.integer('Delay (days)', help="Number of days to do this transition"), + 'invoice_state': fields.selection([ + ("invoiced", "Invoiced"), + ("2binvoiced", "To Be Invoiced"), + ("none", "Not Applicable")], "Invoice Status", + required=True,), + 'picking_type_id': fields.many2one('stock.picking.type', 'Type of the new Operation', required=True, help="This is the picking type associated with the different pickings"), + 'auto': fields.selection( + [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')], + 'Automatic Move', + required=True, select=1, + help="This is used to define paths the product has to follow within the location tree.\n" \ + "The 'Automatic Move' value will create a stock move after the current one that will be "\ + "validated automatically. With 'Manual Operation', the stock move has to be validated "\ + "by a worker. With 'Automatic No Step Added', the location is replaced in the original move." + ), + 'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move is cancelled or split, the move generated by this move will too'), + 'active': fields.related('route_id', 'active', type='boolean', string='Active', store={ + 'stock.location.route': (_get_route, ['active'], 20), + 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),}, + help="If the active field is set to False, it will allow you to hide the rule without removing it." ), + } + _defaults = { + 'auto': 'auto', + 'delay': 1, + 'invoice_state': 'none', + 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c), + 'propagate': True, + 'active': True, + } + def _apply(self, cr, uid, rule, move, context=None): + move_obj = self.pool.get('stock.move') + newdate = (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta(days=rule.delay or 0)).strftime('%Y-%m-%d') + if rule.auto=='transparent': + move_obj.write(cr, uid, [move.id], { + 'date': newdate, + 'location_dest_id': rule.location_dest_id.id + }) + if rule.location_dest_id.id<>move.location_dest_id.id: + move_obj._push_apply(self, cr, uid, move.id, context) + return move.id + else: + move_id = move_obj.copy(cr, uid, move.id, { + 'location_id': move.location_dest_id.id, + 'location_dest_id': rule.location_dest_id.id, + 'date': datetime.now().strftime('%Y-%m-%d'), + 'company_id': rule.company_id and rule.company_id.id or False, + 'date_expected': newdate, + 'picking_id': False, + 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False, + 'rule_id': rule.id, + 'propagate': rule.propagate, + }) + move_obj.write(cr, uid, [move.id], { + 'move_dest_id': move_id, + }) + move_obj.action_confirm(cr, uid, [move_id], context=None) + return move_id + +class stock_move_putaway(osv.osv): + _name = 'stock.move.putaway' + _description = 'Proposed Destination' + _columns = { + 'move_id': fields.many2one('stock.move', required=True), + 'location_id': fields.many2one('stock.location', 'Location', required=True), + 'lot_id': fields.many2one('stock.production.lot', 'Lot'), + 'quantity': fields.float('Quantity', required=True), + } + + # ------------------------- # Packaging related stuff diff --git a/addons/stock/stock_data.xml b/addons/stock/stock_data.xml index 1a43bf8e867..6ad098162f2 100644 --- a/addons/stock/stock_data.xml +++ b/addons/stock/stock_data.xml @@ -61,76 +61,13 @@ customer - - - view - - - - - Output - - internal - - - Stock - - + Inter Warehouse transit - - - WH Picking in - WH\IN - 5 - - - - - WH Picking out - WH\OUT - 5 - - - - - WH Picking internal - WH\INT - 5 - - - - - Receptions - - - - incoming - True - - - - Delivery Orders - - - - outgoing - - - - Internal Transfers - - - - internal - - - - @@ -157,32 +94,12 @@ watch your stock valuation, and track production lots upstream and downstream (b - - - Your Company: Stock → Customer - move - - - - make_to_stock - - - + MTO 10 - - Your Company: Stock → Customer - move - - - - make_to_order - - - @@ -196,11 +113,6 @@ watch your stock valuation, and track production lots upstream and downstream (b - - - - - property_stock_procurement @@ -226,12 +138,14 @@ watch your stock valuation, and track production lots upstream and downstream (b + + - + WH - + Stock orderpoint stock.orderpoint @@ -246,19 +160,5 @@ watch your stock valuation, and track production lots upstream and downstream (b 1 - - - - - - - - - - - - diff --git a/addons/stock/stock_data.yml b/addons/stock/stock_data.yml new file mode 100644 index 00000000000..19fd0ec2f8e --- /dev/null +++ b/addons/stock/stock_data.yml @@ -0,0 +1,5 @@ +- + !python {model: res.partner}: | + main_warehouse = self.pool.get('stock.warehouse').browse(cr, uid, ref('warehouse0'), context=context) + self.write(cr, uid, ref('base.main_partner'), {'property_stock_customer':main_warehouse.lot_stock_id.id}) + diff --git a/addons/stock/stock_demo.xml b/addons/stock/stock_demo.xml index d872443ad11..ff563fdb667 100644 --- a/addons/stock/stock_demo.xml +++ b/addons/stock/stock_demo.xml @@ -34,6 +34,9 @@ customer + + diff --git a/addons/stock/stock_demo.yml b/addons/stock/stock_demo.yml index 203bf1c797b..da5c9cad18b 100644 --- a/addons/stock/stock_demo.yml +++ b/addons/stock/stock_demo.yml @@ -66,9 +66,18 @@ prod_lot_id: lot_icecream_1 location_id: location_refrigerator - - !record {model: stock.picking, id: outgoing_shipment}: - location_dest_id: location_delivery_counter - picking_type_id: picking_type_out + Create STOCK_PICKING for OUT +- + !python {model: stock.picking}: | + main_warehouse = self.pool.get('stock.warehouse').browse(cr, uid, ref('warehouse0'), context=context) + my_picking_type_out = main_warehouse.out_type_id.id + create_id = self.create(cr,uid,{'location_dest_id':ref('location_delivery_counter'),'picking_type_id':my_picking_type_out}) + for xml_record in [{'name':'outgoing_shipment','module':'stock', 'model':'stock.picking','res_id':create_id}]: + xml_ids = self.pool.get('ir.model.data').search(cr, uid, [('module', '=', xml_record['module']), ('model', '=', xml_record['model']), ('name', '=', xml_record['name'])], context=context) + if not xml_ids: + self.pool.get('ir.model.data').create(cr, uid, xml_record, context=context) + #avoid the xml id and the associated resource being dropped by the orm by manually making a hit on it + self.pool.get('ir.model.data')._update_dummy(cr, uid, xml_record['model'], xml_record['module'], xml_record['name']) - !record {model: stock.move, id: outgoing_shipment_icecream}: picking_id: outgoing_shipment @@ -78,10 +87,18 @@ location_id: location_refrigerator location_dest_id: location_delivery_counter - - !record {model: stock.picking, id: incomming_shipment}: - partner_id: base.res_partner_address_9 - location_dest_id: location_refrigerator - picking_type_id: picking_type_in + Create STOCK_PICKING for IN +- + !python {model: stock.picking}: | + main_warehouse = self.pool.get('stock.warehouse').browse(cr, uid, ref('warehouse0'), context=context) + my_picking_type = main_warehouse.in_type_id.id + create_id = self.create(cr,uid,{'location_dest_id':ref('location_refrigerator'),'picking_type_id':my_picking_type,'partner_id':ref('base.res_partner_address_9')}); + for xml_record in [{'name':'incomming_shipment','module':'stock', 'model':'stock.picking','res_id':create_id}]: + xml_ids = self.pool.get('ir.model.data').search(cr, uid, [('module', '=', xml_record['module']), ('model', '=', xml_record['model']), ('name', '=', xml_record['name'])], context=context) + if not xml_ids: + self.pool.get('ir.model.data').create(cr, uid, xml_record, context=context) + #avoid the xml id and the associated resource being dropped by the orm by manually making a hit on it + self.pool.get('ir.model.data')._update_dummy(cr, uid, xml_record['model'], xml_record['module'], xml_record['name']) - !record {model: stock.move, id: incomming_shipment_icecream}: picking_id: incomming_shipment diff --git a/addons/stock/stock_demo_pre.yml b/addons/stock/stock_demo_pre.yml new file mode 100644 index 00000000000..aa3270010c1 --- /dev/null +++ b/addons/stock/stock_demo_pre.yml @@ -0,0 +1,34 @@ +- + !record {model: stock.location, id: stock_location_14}: + name: Shelf 2 + posx: 0 + +- + !record {model: stock.location, id: stock_location_components}: + name: Shelf 1 + posx: 0 + +- + !python {model: ir.model.data}: | + main_warehouse = self.pool.get('stock.warehouse').browse(cr, uid, ref('warehouse0'), context=context) + main_stock = main_warehouse.lot_stock_id.id + self.pool.get('stock.location').write(cr, uid, [ref('stock_location_14'), ref('stock_location_components')], {'location_id':main_stock}, context=context) + + #create xml ids for demo data that are widely used in tests, for more convenience + xml_references = [ + {'name': 'stock_location_stock', 'module': 'stock', 'model': 'stock.location', 'res_id': main_warehouse.lot_stock_id.id}, + {'name': 'stock_location_company', 'module': 'stock', 'model': 'stock.location', 'res_id': main_warehouse.wh_input_stock_loc_id.id}, + {'name':'stock_location_output','module':'stock', 'model':'stock.location','res_id':main_warehouse.wh_output_stock_loc_id.id}, + {'name':'location_pack_zone','module':'stock', 'model':'stock.location','res_id':main_warehouse.wh_pack_stock_loc_id.id}, + {'name':'picking_type_internal','module':'stock', 'model':'stock.picking.type','res_id':main_warehouse.int_type_id.id}, + {'name':'picking_type_in','module':'stock', 'model':'stock.picking.type','res_id':main_warehouse.in_type_id.id}, + {'name':'picking_type_out','module':'stock', 'model':'stock.picking.type','res_id':main_warehouse.out_type_id.id}, + ] + for xml_record in xml_references: + xml_ids = self.search(cr, uid, [('module', '=', xml_record['module']), ('model', '=', xml_record['model']), ('name', '=', xml_record['name'])], context=context) + if not xml_ids: + self.create(cr, uid, xml_record, context=context) + #avoid the xml id and the associated resource being dropped by the orm by manually making a hit on it + self._update_dummy(cr, uid, xml_record['model'], xml_record['module'], xml_record['name']) + + diff --git a/addons/stock_location/stock_location_data.xml b/addons/stock/stock_location_data.xml similarity index 95% rename from addons/stock_location/stock_location_data.xml rename to addons/stock/stock_location_data.xml index e9fb1a8e9ac..1501597f482 100644 --- a/addons/stock_location/stock_location_data.xml +++ b/addons/stock/stock_location_data.xml @@ -3,10 +3,10 @@ - + - + + --> - - Your Company: Reception in 1 step + diff --git a/addons/stock_location/stock_location_demo_cpu1.xml b/addons/stock/stock_location_demo_cpu1.xml similarity index 98% rename from addons/stock_location/stock_location_demo_cpu1.xml rename to addons/stock/stock_location_demo_cpu1.xml index c44e4f738c9..f738f1e4faa 100644 --- a/addons/stock_location/stock_location_demo_cpu1.xml +++ b/addons/stock/stock_location_demo_cpu1.xml @@ -5,6 +5,7 @@ + Quality Location True diff --git a/addons/stock_location/stock_location_demo_cpu3.yml b/addons/stock/stock_location_demo_cpu3.yml similarity index 93% rename from addons/stock_location/stock_location_demo_cpu3.yml rename to addons/stock/stock_location_demo_cpu3.yml index d1d4a2c8348..65ab318df6d 100644 --- a/addons/stock_location/stock_location_demo_cpu3.yml +++ b/addons/stock/stock_location_demo_cpu3.yml @@ -23,4 +23,4 @@ name: Pick List location_from_id: stock.stock_location_output location_dest_id: location_pack_zone - picking_type_id: stock.picking_type_internal \ No newline at end of file + picking_type_id: stock.picking_type_internal diff --git a/addons/stock/stock_orderpoint.xml b/addons/stock/stock_orderpoint.xml index 67d610c65c9..5bbca72765b 100644 --- a/addons/stock/stock_orderpoint.xml +++ b/addons/stock/stock_orderpoint.xml @@ -9,7 +9,7 @@ - + 10.0 @@ -17,7 +17,7 @@ - + 12.0 @@ -25,7 +25,7 @@ - + 50.0 @@ -33,7 +33,7 @@ - + 15.0 @@ -41,7 +41,7 @@ - + 5.0 @@ -49,7 +49,7 @@ - + \ No newline at end of file diff --git a/addons/stock/stock_orderpoint.yml b/addons/stock/stock_orderpoint.yml new file mode 100644 index 00000000000..3c4eb6e9961 --- /dev/null +++ b/addons/stock/stock_orderpoint.yml @@ -0,0 +1,6 @@ +- + !python {model: stock.location}: | + main_warehouse = self.pool.get('stock.warehouse').browse(cr, uid, ref('warehouse0'), context=context) + main_stock = main_warehouse.lot_stock_id.id + stowar_op_ids = self.pool.get('stock.warehouse.orderpoint').search(cr, uid, [], context=context) + self.pool.get('stock.warehouse.orderpoint').write(cr,uid,stowar_op_ids,{'location_id':main_stock}, context=context) diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index 3e4ab8bcc1c..fcc3e14306f 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -390,6 +390,37 @@ + + +
+

+ Removal strategies define the method used for suggesting the + location to take the products from +

+ + + + + + +
+ + + +
+

+ Putaway strategies define the method used for suggesting the + location to put the products +

+ + + + + + + +
+
@@ -422,6 +453,120 @@ + + + product.putaway.form + product.putaway + +
+ + + + + + +
+ + + product.removal.form + product.removal + +
+ + + + + +
+ + + stock.location.path.tree + stock.location.path + + + + + + + + + + + stock.location.path.form + stock.location.path + +
+ + + + + + + + + + + + +
+
+
+ + + product.category.form + product.category + + + + +
+

+ +

+

+ The following routes will apply to the products in this category taking into account parent categories: + +

+
+
+ +
+

+ Removal strategies define the method used for suggesting the + location to take the products from +

+ + + + + + + +
+ + + +
+

+ Putaway strategies define the method used for suggesting the + location to put the products +

+ + + + + + + +
+
+
+
+ Locations @@ -503,19 +648,34 @@
- +
@@ -1119,7 +1279,7 @@ ir.actions.act_window form tree,form - + {'product_receive': True, 'search_default_future': True} @@ -1488,6 +1648,9 @@ + + + @@ -1543,12 +1706,18 @@ + + + @@ -1866,6 +2035,20 @@ + + stock.location.route.tree + stock.location.route + + + + + + + + + + + stock.location.route.form stock.location.route @@ -1880,8 +2063,23 @@ + + +

Check these box so that the user can select this route during the following operations

+ + + + + + + + + + + + @@ -1889,13 +2087,56 @@
+ + + Routes + stock.location.route + ir.actions.act_window + form + tree,form + + +

+ Click to add a route. +

+

You can define here the main routes that run through + your warehouses and that define the flows of your products. These + routes can be assigned to a product, a product category or be fixed + on procurement or sales order.

+
+
+ + + + + procurement.order.form.view.inherit + + procurement.order + + + + + + + + + product.template.procurement.rule.inherit + product.product + + + +