[REF] stock: refactored the removal and putaway strategies + added FEFO removal in...
authorQuentin (OpenERP) <qdp-launchpad@openerp.com>
Fri, 11 Apr 2014 17:19:01 +0000 (19:19 +0200)
committerQuentin (OpenERP) <qdp-launchpad@openerp.com>
Fri, 11 Apr 2014 17:19:01 +0000 (19:19 +0200)
bzr revid: qdp-launchpad@openerp.com-20140411171901-ibjelg7wldld167y

addons/product_expiry/__openerp__.py
addons/product_expiry/product_expiry.py
addons/product_expiry/product_expiry_data.xml [new file with mode: 0644]
addons/stock/product.py
addons/stock/stock.py
addons/stock/stock_data.xml
addons/stock/stock_view.xml

index ae65c90..f14a837 100644 (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'],
index 4d8fd49..7b41859 100644 (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 = {
diff --git a/addons/product_expiry/product_expiry_data.xml b/addons/product_expiry/product_expiry_data.xml
new file mode 100644 (file)
index 0000000..b2ce4a4
--- /dev/null
@@ -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>
+
index d1b8691..588937f 100644 (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),
     }
 
index 3c7f849..c9d6248 100644 (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)
-
-        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)
+        ''' 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
+
+    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
 
index 332c213..a13c385 100644 (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
index 3889f79..f1c0bb5 100644 (file)
                             <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>
             <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>
             <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>
             <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>