From 2fd72600fabf3faa47883e3dc1cb59c962671fb1 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Fri, 11 Apr 2014 19:19:01 +0200 Subject: [PATCH] [REF] stock: refactored the removal and putaway strategies + added FEFO removal in product_expiry bzr revid: qdp-launchpad@openerp.com-20140411171901-ibjelg7wldld167y --- addons/product_expiry/__openerp__.py | 5 +- addons/product_expiry/product_expiry.py | 14 +++ addons/product_expiry/product_expiry_data.xml | 10 ++ addons/stock/product.py | 57 ++++++++--- addons/stock/stock.py | 90 ++++++++--------- addons/stock/stock_data.xml | 8 ++ addons/stock/stock_view.xml | 99 +++++-------------- 7 files changed, 147 insertions(+), 136 deletions(-) create mode 100644 addons/product_expiry/product_expiry_data.xml diff --git a/addons/product_expiry/__openerp__.py b/addons/product_expiry/__openerp__.py index ae65c9092b3..f14a837f162 100644 --- a/addons/product_expiry/__openerp__.py +++ b/addons/product_expiry/__openerp__.py @@ -35,8 +35,9 @@ Following dates can be tracked: - removal date - alert date -Used, for example, in food industries.""", - 'data' : ['product_expiry_view.xml'], +Also implements the removal strategy First Expiry First Out (FEFO) widely used, for example, in food industries. +""", + 'data' : ['product_expiry_view.xml', 'product_expiry_data.xml'], 'auto_install': False, 'installable': True, 'images': ['images/production_lots_dates.jpeg','images/products_dates.jpeg'], diff --git a/addons/product_expiry/product_expiry.py b/addons/product_expiry/product_expiry.py index 4d8fd49ec4f..7b418598728 100644 --- a/addons/product_expiry/product_expiry.py +++ b/addons/product_expiry/product_expiry.py @@ -75,6 +75,20 @@ class stock_production_lot(osv.osv): 'alert_date': _get_date('alert_time'), } + +class stock_quant(osv.osv): + _inherit = 'stock.quant' + _column = { + 'removal_date': fields.related('lot_id', 'removal_date', type='date', string='Removal Date', store=True), + } + + def apply_removal_strategy(self, cr, uid, location, product, qty, domain, removal_strategy, context=None): + if removal_strategy == 'fefo': + order = 'removal_date, id' + return self._quants_get_order(cr, uid, location, product, qty, domain, order, context=context) + return super(stock_quant, self).apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context) + + class product_product(osv.osv): _inherit = 'product.product' _columns = { diff --git a/addons/product_expiry/product_expiry_data.xml b/addons/product_expiry/product_expiry_data.xml new file mode 100644 index 00000000000..b2ce4a4c95a --- /dev/null +++ b/addons/product_expiry/product_expiry_data.xml @@ -0,0 +1,10 @@ + + + + + First Expiry First Out (FEFO) + fefo + + + + diff --git a/addons/stock/product.py b/addons/stock/product.py index d1b8691e0cc..588937f93d2 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -323,37 +323,63 @@ 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), + 'name': fields.char('Name', required=True), + 'method': fields.char("Method", required=True, help="FIFO, LIFO..."), } class product_putaway_strategy(osv.osv): _name = 'product.putaway' _description = 'Put Away Strategy' + + def _get_putaway_options(self, cr, uid, context=None): + return [('fixed', 'Fixed Location')] + _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')], + 'name': fields.char('Name', required=True), + 'method': fields.selection(_get_putaway_options, "Method", required=True), + 'fixed_location_ids': fields.one2many('stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', help="When the method is fixed, this location will be used to store the products"), + } + + _defaults = { + 'method': 'fixed', + } + + def putaway_apply(self, cr, uid, putaway_strat, product, context=None): + if putaway_strat.method == 'fixed': + all_parent_categs = [] + categ = product.categ_id + while categ: + all_parent_categs.append(categ.id) + categ = categ.parent_id + for strat in putaway_strat.fixed_location_ids: + if strat.category_id.id in all_parent_categs: + return strat.fixed_location_id.id + + +class fixed_putaway_strat(osv.osv): + _name = 'stock.fixed.putaway.strat' + _order = 'sequence' + _columns = { + 'putaway_id': fields.many2one('product.putaway', 'Put Away Method', required=True), + 'category_id': fields.many2one('product.category', 'Product Category', required=True), + 'fixed_location_id': fields.many2one('stock.location', 'Location', required=True), + 'sequence': fields.integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list."), } 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] @@ -362,11 +388,10 @@ class product_category(osv.osv): 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'), + 'removal_strategy_id': fields.many2one('product.removal', 'Force Removal Strategy', help="Set a specific removal strategy that will be used regardless of the source location for this product category"), 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True), } diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 3c7f8499293..c9d6248c73a 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -129,9 +129,9 @@ 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('Is a 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'), - 'loc_barcode': fields.char('Location barcode'), + 'removal_strategy_id': fields.many2one('product.removal', 'Removal Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to take the products from, which lot etc. for this location. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."), + 'putaway_strategy_id': fields.many2one('product.putaway', 'Put Away Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to store the products. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."), + 'loc_barcode': fields.char('Location Barcode'), } _defaults = { 'active': True, @@ -147,31 +147,36 @@ class stock_location(osv.osv): def create(self, cr, uid, default, context=None): if not default.get('loc_barcode', False): default.update({'loc_barcode': default.get('complete_name', False)}) - return super(stock_location,self).create(cr, uid, default, context=context) + return super(stock_location, self).create(cr, uid, default, context=context) 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) + ''' Returns the location where the product has to be put, if any compliant putaway strategy is found. Otherwise returns None.''' + putaway_obj = self.pool.get('product.putaway') + loc = location + while loc: + if loc.putaway_strategy_id: + res = putaway_obj.putaway_strat_apply(cr, uid, loc.putaway_strategy_id, product, context=context) + if res: + return res + loc = loc.location_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 _default_removal_strategy(self, cr, uid, context=None): + return 'fifo' def get_removal_strategy(self, cr, uid, location, product, context=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 + ''' Returns the removal strategy to consider for the given product and location. + :param location: browse record (stock.location) + :param product: browse record (product.product) + :rtype: char + ''' + if product.categ_id.removal_strategy_id: + return product.categ_id.removal_strategy_id.method + loc = location + while loc: + if loc.removal_strategy_id: + return loc.removal_strategy_id.method + loc = loc.location_id + return self._default_removal_strategy(cr, uid, context=context) #---------------------------------------------------------- @@ -419,15 +424,19 @@ class stock_quant(osv.osv): if restrict_lot_id: domain += [('lot_id', '=', restrict_lot_id)] if location: - removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) or 'fifo' - if removal_strategy == 'fifo': - result += self._quants_get_fifo(cr, uid, location, product, qty, domain, context=context) - elif removal_strategy == 'lifo': - result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context) - else: - raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,))) + removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) + result += self.apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context) return result + def apply_removal_strategy(self, cr, uid, location, product, quantity, domain, removal_strategy, context=None): + if removal_strategy == 'fifo': + order = 'in_date, id' + return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) + elif removal_strategy == 'lifo': + order = 'in_date desc, id desc' + return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) + raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,))) + def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location=False, context=None): '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location. ''' @@ -574,14 +583,6 @@ class stock_quant(osv.osv): offset += 10 return res - def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None): - order = 'in_date, id' - return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) - - def _quants_get_lifo(self, cr, uid, location, product, quantity, domain=[], context=None): - order = 'in_date desc, id desc' - return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context) - def _check_location(self, cr, uid, location, context=None): if location.usage == 'view': raise osv.except_osv(_('Error'), _('You cannot move to a location of type view %s.') % (location.name)) @@ -922,8 +923,10 @@ class stock_picking(osv.osv): self.do_prepare_partial(cr, uid, picking_ids, context=context) def _picking_putaway_resolution(self, cr, uid, picking, product, putaway, context=None): - if putaway.method == 'fixed' and putaway.location_spec_id: - return putaway.location_spec_id.id + if putaway.method == 'fixed': + for strat in putaway.fixed_location_ids: + if product.categ_id.id == strat.category_id.id: + return strat.fixed_location_id.id return False def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None): @@ -981,11 +984,10 @@ class stock_picking(osv.osv): location = False # Search putaway strategy if product_putaway_strats.get(product.id): - putaway_strat = product_putaway_strats[product.id] + location = product_putaway_strats[product.id] else: - putaway_strat = self.pool.get('stock.location').get_putaway_strategy(cr, uid, picking.location_dest_id, product, context=context) - product_putaway_strats[product.id] = putaway_strat - if putaway_strat: + location = self.pool.get('stock.location').get_putaway_strategy(cr, uid, picking.location_dest_id, product, context=context) + product_putaway_strats[product.id] = location location = self._picking_putaway_resolution(cr, uid, picking, product, putaway_strat, context=context) return location or picking.picking_type_id.default_location_dest_id.id or picking.location_dest_id.id diff --git a/addons/stock/stock_data.xml b/addons/stock/stock_data.xml index 332c2130d05..a13c385e68b 100644 --- a/addons/stock/stock_data.xml +++ b/addons/stock/stock_data.xml @@ -2,6 +2,14 @@ + + First In First Out (FIFO) + fifo + + + Last In First Out (LIFO) + lifo +