[REF] stock: refactored the removal and putaway strategies + added FEFO removal in product_expiry

bzr revid: qdp-launchpad@openerp.com-20140411171901-ibjelg7wldld167y
This commit is contained in:
Quentin (OpenERP) 2014-04-11 19:19:01 +02:00
parent d31e02fb3f
commit 2fd72600fa
7 changed files with 147 additions and 136 deletions

View File

@ -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'],

View File

@ -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 = {

View File

@ -0,0 +1,10 @@
<?xml version="1.0" ?>
<openerp>
<data>
<record id="removal_fefo" model="product.removal">
<field name="name">First Expiry First Out (FEFO)</field>
<field name="method">fefo</field>
</record>
</data>
</openerp>

View File

@ -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),
}

View File

@ -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

View File

@ -2,6 +2,14 @@
<openerp>
<data>
<record id="removal_fifo" model="product.removal">
<field name="name">First In First Out (FIFO)</field>
<field name="method">fifo</field>
</record>
<record id="removal_lifo" model="product.removal">
<field name="name">Last In First Out (LIFO)</field>
<field name="method">lifo</field>
</record>
<!--
Resource: stock.location

View File

@ -383,38 +383,11 @@
<field name="posz"/>
<field name="loc_barcode"/>
</group>
<group string="Logistics" groups="stock.group_adv_location">
<field name="removal_strategy_id" options="{'no_create': True}"/>
<field name="putaway_strategy_id"/>
</group>
</group>
<separator string="Removal Strategies" groups="stock.group_adv_location"/>
<group groups="stock.group_adv_location">
<div class="oe_inline">
<p class="oe_grey">
Removal strategies define the method used for suggesting the
location to take the products from
</p>
<field name="removal_strategy_ids" class ="oe_inline">
<tree editable="bottom" string="removal">
<field name="product_categ_id"/>
<field name="method"/>
</tree>
</field>
</div>
<newline/>
<separator string="Putaway Strategies"/>
<newline/>
<div class="oe_inline">
<p class="oe_grey">
Putaway strategies define the method used for suggesting the
location to put the products
</p>
<field name="putaway_strategy_ids" class="oe_inline">
<tree string="Put Away" editable="bottom">
<field name="product_categ_id"/>
<field name="method"/>
<field name="location_spec_id"/>
</tree>
</field>
</div>
</group>
<separator string="Additional Information"/>
<field name="comment"/>
</form>
@ -452,11 +425,21 @@
<field name="name">product.putaway.form</field>
<field name="model">product.putaway</field>
<field name="arch" type="xml">
<form string="Putaway">
<field name="product_categ_id"/>
<field name="location_id"/>
<field name="method"/>
<field name="location_spec_id"/>
<form string="Putaway" version="7.0">
<group colspan="4">
<field name="name"/>
<field name="method"/>
</group>
<div attrs="{'invisible': [('method', '!=', 'fixed')]}">
<separator string="Fixed Locations Per Categories"/>
<field name="fixed_location_ids" colspan="4" nolabel="1">
<tree editable="top">
<field name="sequence" widget='handle'/>
<field name="category_id"/>
<field name="fixed_location_id"/>
</tree>
</field>
</div>
</form>
</field>
</record>
@ -466,8 +449,7 @@
<field name="model">product.removal</field>
<field name="arch" type="xml">
<form string="Removal">
<field name="product_categ_id"/>
<field name="location_id"/>
<field name="name"/>
<field name="method"/>
</form>
</field>
@ -515,48 +497,17 @@
<field name="model">product.category</field>
<field name="inherit_id" ref="product.product_category_form_view" />
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<group string="Routes" colspan="4">
<div class="oe_inline">
<p attrs="{'invisible':[('route_ids','=',False)]}">
<field name="route_ids" nolabel="1" widget="many2many_tags" class="oe_inline"/>
</p>
<xpath expr="//group[@name='parent']" position="inside">
<group string="Logistics" colspan="2">
<field name="route_ids" widget="many2many_tags"/>
<div class="oe_inline" colspan="2">
<p attrs="{'invisible':[('parent_id','=',False)]}">
The following routes will apply to the products in this category taking into account parent categories:
<field name="total_route_ids" nolabel="1" widget="many2many_tags"/>
</p>
</div>
<field name="removal_strategy_id" options="{'no_create': True}"/>
</group>
<separator string="Removal Strategies"/>
<div class="oe_inline">
<p class="oe_grey">
Removal strategies define the method used for suggesting the
location to take the products from
</p>
<field name="removal_strategy_ids">
<tree editable="bottom" string="removal">
<field name="location_id"/>
<field name="method"/>
</tree>
</field>
</div>
<newline/>
<separator string="Putaway Strategies"/>
<newline/>
<div class="oe_inline">
<p class="oe_grey">
Putaway strategies define the method used for suggesting the
location to put the products
</p>
<field name="putaway_strategy_ids">
<tree string="Put Away" editable="bottom">
<field name="location_id"/>
<field name="method"/>
<field name="location_spec_id"/>
</tree>
</field>
</div>
</xpath>
</field>
</record>