[MERGE] forward port of branch 8.0 up to 2e092ac
[odoo/odoo.git] / addons / stock / product.py
index b13192c..23f4d3f 100644 (file)
 
 from openerp.osv import fields, osv
 from openerp.tools.translate import _
+from openerp.tools.safe_eval import safe_eval as eval
 import openerp.addons.decimal_precision as dp
+from openerp.tools.float_utils import float_round
 
 class product_product(osv.osv):
     _inherit = "product.product"
-
+        
     def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None):
         res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids])
         move_pool=self.pool.get('stock.move')
         moves = move_pool.read_group(cr, uid, [
             ('product_id', 'in', ids),
-            ('picking_id.type', '=', 'in'),
+            ('location_id.usage', '!=', 'internal'),
+            ('location_dest_id.usage', '=', 'internal'),
             ('state','in',('confirmed','assigned','pending'))
         ], ['product_id'], ['product_id'])
         for move in moves:
@@ -39,7 +42,8 @@ class product_product(osv.osv):
             res[product_id]['reception_count'] = move['product_id_count']
         moves = move_pool.read_group(cr, uid, [
             ('product_id', 'in', ids),
-            ('picking_id.type', '=', 'out'),
+            ('location_id.usage', '=', 'internal'),
+            ('location_dest_id.usage', '!=', 'internal'),
             ('state','in',('confirmed','assigned','pending'))
         ], ['product_id'], ['product_id'])
         for move in moves:
@@ -47,146 +51,6 @@ class product_product(osv.osv):
             res[product_id]['delivery_count'] = move['product_id_count']
         return res
 
-    def get_product_accounts(self, cr, uid, product_id, context=None):
-        """ To get the stock input account, stock output account and stock journal related to product.
-        @param product_id: product id
-        @return: dictionary which contains information regarding stock input account, stock output account and stock journal
-        """
-        if context is None:
-            context = {}
-        product_obj = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
-
-        stock_input_acc = product_obj.property_stock_account_input and product_obj.property_stock_account_input.id or False
-        if not stock_input_acc:
-            stock_input_acc = product_obj.categ_id.property_stock_account_input_categ and product_obj.categ_id.property_stock_account_input_categ.id or False
-
-        stock_output_acc = product_obj.property_stock_account_output and product_obj.property_stock_account_output.id or False
-        if not stock_output_acc:
-            stock_output_acc = product_obj.categ_id.property_stock_account_output_categ and product_obj.categ_id.property_stock_account_output_categ.id or False
-
-        journal_id = product_obj.categ_id.property_stock_journal and product_obj.categ_id.property_stock_journal.id or False
-        account_valuation = product_obj.categ_id.property_stock_valuation_account_id and product_obj.categ_id.property_stock_valuation_account_id.id or False
-        return {
-            'stock_account_input': stock_input_acc,
-            'stock_account_output': stock_output_acc,
-            'stock_journal': journal_id,
-            'property_stock_valuation_account_id': account_valuation
-        }
-
-    def do_change_standard_price(self, cr, uid, ids, datas, context=None):
-        """ Changes the Standard Price of Product and creates an account move accordingly.
-        @param datas : dict. contain default datas like new_price, stock_output_account, stock_input_account, stock_journal
-        @param context: A standard dictionary
-        @return:
-
-        """
-        location_obj = self.pool.get('stock.location')
-        move_obj = self.pool.get('account.move')
-        move_line_obj = self.pool.get('account.move.line')
-        if context is None:
-            context = {}
-
-        new_price = datas.get('new_price', 0.0)
-        stock_output_acc = datas.get('stock_output_account', False)
-        stock_input_acc = datas.get('stock_input_account', False)
-        journal_id = datas.get('stock_journal', False)
-        product_obj=self.browse(cr, uid, ids, context=context)[0]
-        account_valuation = product_obj.categ_id.property_stock_valuation_account_id
-        account_valuation_id = account_valuation and account_valuation.id or False
-        if not account_valuation_id: raise osv.except_osv(_('Error!'), _('Specify valuation Account for Product Category: %s.') % (product_obj.categ_id.name))
-        move_ids = []
-        loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
-        for rec_id in ids:
-            for location in location_obj.browse(cr, uid, loc_ids, context=context):
-                c = context.copy()
-                c.update({
-                    'location': location.id,
-                    'compute_child': False
-                })
-
-                product = self.browse(cr, uid, rec_id, context=c)
-                qty = product.qty_available
-                diff = product.standard_price - new_price
-                if not diff: raise osv.except_osv(_('Error!'), _("No difference between standard price and new price!"))
-                if qty:
-                    company_id = location.company_id and location.company_id.id or False
-                    if not company_id: raise osv.except_osv(_('Error!'), _('Please specify company in Location.'))
-                    #
-                    # Accounting Entries
-                    #
-                    if not journal_id:
-                        journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
-                    if not journal_id:
-                        raise osv.except_osv(_('Error!'),
-                            _('Please define journal '\
-                              'on the product category: "%s" (id: %d).') % \
-                                (product.categ_id.name,
-                                    product.categ_id.id,))
-                    move_id = move_obj.create(cr, uid, {
-                                'journal_id': journal_id,
-                                'company_id': company_id
-                                })
-
-                    move_ids.append(move_id)
-
-
-                    if diff > 0:
-                        if not stock_input_acc:
-                            stock_input_acc = product.\
-                                property_stock_account_input.id
-                        if not stock_input_acc:
-                            stock_input_acc = product.categ_id.\
-                                    property_stock_account_input_categ.id
-                        if not stock_input_acc:
-                            raise osv.except_osv(_('Error!'),
-                                    _('Please define stock input account ' \
-                                            'for this product: "%s" (id: %d).') % \
-                                            (product.name,
-                                                product.id,))
-                        amount_diff = qty * diff
-                        move_line_obj.create(cr, uid, {
-                                    'name': product.name,
-                                    'account_id': stock_input_acc,
-                                    'debit': amount_diff,
-                                    'move_id': move_id,
-                                    })
-                        move_line_obj.create(cr, uid, {
-                                    'name': product.categ_id.name,
-                                    'account_id': account_valuation_id,
-                                    'credit': amount_diff,
-                                    'move_id': move_id
-                                    })
-                    elif diff < 0:
-                        if not stock_output_acc:
-                            stock_output_acc = product.\
-                                property_stock_account_output.id
-                        if not stock_output_acc:
-                            stock_output_acc = product.categ_id.\
-                                    property_stock_account_output_categ.id
-                        if not stock_output_acc:
-                            raise osv.except_osv(_('Error!'),
-                                    _('Please define stock output account ' \
-                                            'for this product: "%s" (id: %d).') % \
-                                            (product.name,
-                                                product.id,))
-                        amount_diff = qty * -diff
-                        move_line_obj.create(cr, uid, {
-                                        'name': product.name,
-                                        'account_id': stock_output_acc,
-                                        'credit': amount_diff,
-                                        'move_id': move_id
-                                    })
-                        move_line_obj.create(cr, uid, {
-                                        'name': product.categ_id.name,
-                                        'account_id': account_valuation_id,
-                                        'debit': amount_diff,
-                                        'move_id': move_id
-                                    })
-
-            self.write(cr, uid, rec_id, {'standard_price': new_price})
-
-        return move_ids
-
     def view_header_get(self, cr, user, view_id, view_type, context=None):
         if context is None:
             context = {}
@@ -196,221 +60,147 @@ class product_product(osv.osv):
             return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
         return res
 
-    def _get_locations_from_context(self, cr, uid, ids, context=None):
-        if context is None:
-            context = {}
+    def _get_domain_locations(self, cr, uid, ids, context=None):
+        '''
+        Parses the context and returns a list of location_ids based on it.
+        It will return all stock locations when no parameters are given
+        Possible parameters are shop, warehouse, location, force_company, compute_child
+        '''
+        context = context or {}
+
         location_obj = self.pool.get('stock.location')
         warehouse_obj = self.pool.get('stock.warehouse')
-        shop_obj = self.pool.get('sale.shop')
-        if context.get('shop', False):
-            warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
-            if warehouse_id:
-                context['warehouse'] = warehouse_id
-
-        if context.get('warehouse', False):
-            lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
-            if lot_id:
-                context['location'] = lot_id
 
+        location_ids = []
         if context.get('location', False):
             if type(context['location']) == type(1):
                 location_ids = [context['location']]
             elif type(context['location']) in (type(''), type(u'')):
-                location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
+                domain = [('complete_name','ilike',context['location'])]
+                if context.get('force_company', False):
+                    domain += [('company_id', '=', context['force_company'])]
+                location_ids = location_obj.search(cr, uid, domain, context=context)
             else:
                 location_ids = context['location']
         else:
-            location_ids = []
-            wids = warehouse_obj.search(cr, uid, [], context=context)
-            if not wids:
-                return False
+            if context.get('warehouse', False):
+                wids = [context['warehouse']]
+            else:
+                wids = warehouse_obj.search(cr, uid, [], context=context)
+
             for w in warehouse_obj.browse(cr, uid, wids, context=context):
-                location_ids.append(w.lot_stock_id.id)
-
-        # build the list of ids of children of the location given by id
-        if context.get('compute_child',True):
-            child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
-            location_ids = child_location_ids or location_ids
-            
-        return location_ids
-
-
-    def _get_date_query(self, cr, uid, ids, context):
-        from_date = context.get('from_date',False)
-        to_date = context.get('to_date',False)
-        date_str = False
-        date_values = False
-        whereadd = []
-        
-        if from_date and to_date:
-            date_str = "date>=%s and date<=%s"
-            whereadd.append(tuple([from_date]))
-            whereadd.append(tuple([to_date]))
-        elif from_date:
-            date_str = "date>=%s"
-            whereadd.append(tuple([from_date]))
-        elif to_date:
-            date_str = "date<=%s"
-            whereadd.append(tuple([to_date]))
-        return (whereadd, date_str)
+                location_ids.append(w.view_location_id.id)
+
+        operator = context.get('compute_child', True) and 'child_of' or 'in'
+        domain = context.get('force_company', False) and ['&', ('company_id', '=', context['force_company'])] or []
+        return (
+            domain + [('location_id', operator, location_ids)],
+            domain + ['&', ('location_dest_id', operator, location_ids), '!', ('location_id', operator, location_ids)],
+            domain + ['&', ('location_id', operator, location_ids), '!', ('location_dest_id', operator, location_ids)]
+        )
+
+    def _get_domain_dates(self, cr, uid, ids, context):
+        from_date = context.get('from_date', False)
+        to_date = context.get('to_date', False)
+        domain = []
+        if from_date:
+            domain.append(('date', '>=', from_date))
+        if to_date:
+            domain.append(('date', '<=', to_date))
+        return domain
 
+    def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
+        context = context or {}
+        field_names = field_names or []
+
+        domain_products = [('product_id', 'in', ids)]
+        domain_quant, domain_move_in, domain_move_out = self._get_domain_locations(cr, uid, ids, context=context)
+        domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products
+        domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products
+        domain_quant += domain_products
+        if context.get('lot_id') or context.get('owner_id') or context.get('package_id'):
+            if context.get('lot_id'):
+                domain_quant.append(('lot_id', '=', context['lot_id']))
+            if context.get('owner_id'):
+                domain_quant.append(('owner_id', '=', context['owner_id']))
+            if context.get('package_id'):
+                domain_quant.append(('package_id', '=', context['package_id']))
+            moves_in = []
+            moves_out = []
+        else:
+            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))
+        res = {}
+        for product in self.browse(cr, uid, ids, context=context):
+            id = product.id
+            qty_available = float_round(quants.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            incoming_qty = float_round(moves_in.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            outgoing_qty = float_round(moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            virtual_available = float_round(quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding)
+            res[id] = {
+                'qty_available': qty_available,
+                'incoming_qty': incoming_qty,
+                'outgoing_qty': outgoing_qty,
+                'virtual_available': virtual_available,
+            }
+        return res
 
-    def get_product_available(self, cr, uid, ids, context=None):
-        """ Finds whether product is available or not in particular warehouse.
-        @return: Dictionary of values
-        """
-        if context is None:
-            context = {}
-        location_obj = self.pool.get('stock.location')
-        warehouse_obj = self.pool.get('stock.warehouse')
-        shop_obj = self.pool.get('sale.shop')
-        
-        states = context.get('states',[])
-        what = context.get('what',())
-        if not ids:
-            ids = self.search(cr, uid, [])
-        res = {}.fromkeys(ids, 0.0)
-        if not ids:
-            return res
-        #set_context: refactor code here
-        location_ids = self._get_locations_from_context(cr, uid, ids, context=context)
-        if not location_ids: #in case of no locations, query will be empty anyways
-            return res
-        
-        # this will be a dictionary of the product UoM by product id
-        product2uom = {}
-        uom_ids = []
-        for product in self.read(cr, uid, ids, ['uom_id'], context=context):
-            product2uom[product['id']] = product['uom_id'][0]
-            uom_ids.append(product['uom_id'][0])
-        # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
-        uoms_o = {}
-        for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
-            uoms_o[uom.id] = uom
-
-        results = []
-        results2 = []
-
-        where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
-
-        where_add, date_str = self._get_date_query(cr, uid, ids, context=context)
-        if where_add:
-            where += where_add
-
-        #It depends on the company of the user OR by using force_company in context
-        user = self.pool.get("res.users").browse(cr, uid, uid, context=context)
-        if context.get("force_company", False):
-            where.append(context['force_company'])
-        else:
-            where.append(user.company_id.id)
-
-        prodlot_id = context.get('prodlot_id', False)
-        prodlot_clause = ''
-        if prodlot_id:
-            prodlot_clause = ' and prodlot_id = %s '
-            where += [prodlot_id]
-
-        # TODO: perhaps merge in one query.
-        if 'in' in what:
-            # all moves from a location out of the set to a location in the set
-            cr.execute(
-                'select sum(product_qty), product_id, product_uom '\
-                'from stock_move '\
-                'where location_id NOT IN %s '\
-                'and location_dest_id IN %s '\
-                'and product_id IN %s '\
-                'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
-                'and company_id = %s '\
-                + prodlot_clause + 
-                'group by product_id,product_uom',tuple(where))
-            results = cr.fetchall()
-        if 'out' in what:
-            # all moves from a location in the set to a location out of the set
-            cr.execute(
-                'select sum(product_qty), product_id, product_uom '\
-                'from stock_move '\
-                'where location_id IN %s '\
-                'and location_dest_id NOT IN %s '\
-                'and product_id  IN %s '\
-                'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
-                'and company_id = %s '\
-                + prodlot_clause + 
-                'group by product_id,product_uom',tuple(where))
-            results2 = cr.fetchall()
-            
-        # Get the missing UoM resources
-        uom_obj = self.pool.get('product.uom')
-        uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
-        if context.get('uom', False):
-            uoms += [context['uom']]
-        uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
-        if uoms:
-            uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
-            for o in uoms:
-                uoms_o[o.id] = o
-                
-        #TOCHECK: before change uom of product, stock move line are in old uom.
-        context.update({'raise-exception': False})
-        # Count the incoming quantities
-        for amount, prod_id, prod_uom in results:
-            amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
-                     uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
-            res[prod_id] += amount
-        # Count the outgoing quantities
-        for amount, prod_id, prod_uom in results2:
-            amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
-                    uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
-            res[prod_id] -= amount
+    def _search_product_quantity(self, cr, uid, obj, name, domain, context):
+        res = []
+        for field, operator, value in domain:
+            #to prevent sql injections
+            assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
+            assert operator in ('<', '>', '=', '!=', '<=', '>='), 'Invalid domain operator'
+            assert isinstance(value, (float, int)), 'Invalid domain right operand'
+
+            if operator == '=':
+                operator = '=='
+
+            product_ids = self.search(cr, uid, [], context=context)
+            ids = []
+            if product_ids:
+                #TODO: use a query instead of this browse record which is probably making the too much requests, but don't forget
+                #the context that can be set with a location, an owner...
+                for element in self.browse(cr, uid, product_ids, context=context):
+                    if eval(str(element[field]) + operator + str(value)):
+                        ids.append(element.id)
+            res.append(('id', 'in', ids))
         return res
 
-    def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
-        """ Finds the incoming and outgoing quantity of product.
-        @return: Dictionary of values
-        """
-        if not field_names:
-            field_names = []
-        if context is None:
-            context = {}
+    def _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None):
         res = {}
-        for id in ids:
-            res[id] = {}.fromkeys(field_names, 0.0)
-        for f in field_names:
-            c = context.copy()
-            if f == 'qty_available':
-                c.update({ 'states': ('done',), 'what': ('in', 'out') })
-            if f == 'virtual_available':
-                c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
-            if f == 'incoming_qty':
-                c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
-            if f == 'outgoing_qty':
-                c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
-            stock = self.get_product_available(cr, uid, ids, context=c)
-            for id in ids:
-                res[id][f] = stock.get(id, 0.0)
+        for product in self.browse(cr, uid, ids, context=context):
+            res[product.id] = str(product.qty_available) +  _(" On Hand")
         return res
 
     _columns = {
-        'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
+        'reception_count': fields.function(_stock_move_count, string="Receipt", type='integer', multi='pickings'),
         'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
         'qty_available': fields.function(_product_available, multi='qty_available',
-            type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
+            type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
             string='Quantity On Hand',
+            fnct_search=_search_product_quantity,
             help="Current quantity of products.\n"
                  "In a context with a single Stock Location, this includes "
                  "goods stored at this Location, or any of its children.\n"
                  "In a context with a single Warehouse, this includes "
                  "goods stored in the Stock Location of this Warehouse, or any "
                  "of its children.\n"
-                 "In a context with a single Shop, this includes goods "
                  "stored in the Stock Location of the Warehouse of this Shop, "
                  "or any of its children.\n"
                  "Otherwise, this includes goods stored in any Stock Location "
                  "with 'internal' type."),
+        'qty_available2': fields.related('qty_available', type="float", relation="product.product", string="On Hand"),
         'virtual_available': fields.function(_product_available, multi='qty_available',
-            type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
-            string='Forecasted Quantity',
+            type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
+            string='Forecast Quantity',
+            fnct_search=_search_product_quantity,
             help="Forecast quantity (computed as Quantity On Hand "
                  "- Outgoing + Incoming)\n"
                  "In a context with a single Stock Location, this includes "
@@ -418,53 +208,35 @@ class product_product(osv.osv):
                  "In a context with a single Warehouse, this includes "
                  "goods stored in the Stock Location of this Warehouse, or any "
                  "of its children.\n"
-                 "In a context with a single Shop, this includes goods "
-                 "stored in the Stock Location of the Warehouse of this Shop, "
-                 "or any of its children.\n"
                  "Otherwise, this includes goods stored in any Stock Location "
                  "with 'internal' type."),
         'incoming_qty': fields.function(_product_available, multi='qty_available',
-            type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
+            type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
             string='Incoming',
+            fnct_search=_search_product_quantity,
             help="Quantity of products that are planned to arrive.\n"
                  "In a context with a single Stock Location, this includes "
                  "goods arriving to this Location, or any of its children.\n"
                  "In a context with a single Warehouse, this includes "
                  "goods arriving to the Stock Location of this Warehouse, or "
                  "any of its children.\n"
-                 "In a context with a single Shop, this includes goods "
-                 "arriving to the Stock Location of the Warehouse of this "
-                 "Shop, or any of its children.\n"
                  "Otherwise, this includes goods arriving to any Stock "
                  "Location with 'internal' type."),
         'outgoing_qty': fields.function(_product_available, multi='qty_available',
-            type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
+            type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
             string='Outgoing',
+            fnct_search=_search_product_quantity,
             help="Quantity of products that are planned to leave.\n"
                  "In a context with a single Stock Location, this includes "
                  "goods leaving this Location, or any of its children.\n"
                  "In a context with a single Warehouse, this includes "
                  "goods leaving the Stock Location of this Warehouse, or "
                  "any of its children.\n"
-                 "In a context with a single Shop, this includes goods "
-                 "leaving the Stock Location of the Warehouse of this "
-                 "Shop, or any of its children.\n"
                  "Otherwise, this includes goods leaving any Stock "
                  "Location with 'internal' type."),
-        'track_production': fields.boolean('Track Manufacturing Lots', help="Forces to specify a Serial Number for all moves containing this product and generated by a Manufacturing Order"),
-        'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to specify a Serial Number for all moves containing this product and coming from a Supplier Location"),
-        'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Serial Number for all moves containing this product and going to a Customer Location"),
         'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
         'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
-        'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
-                                        ('real_time','Real Time (automated)'),], 'Inventory Valuation',
-                                        help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
-                                             "The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products."
-                                        , required=True),
-    }
-
-    _defaults = {
-        'valuation': 'manual_periodic',
+        'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'),
     }
 
     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
@@ -477,7 +249,7 @@ class product_product(osv.osv):
             if fields:
                 if location_info.usage == 'supplier':
                     if fields.get('virtual_available'):
-                        res['fields']['virtual_available']['string'] = _('Future Receptions')
+                        res['fields']['virtual_available']['string'] = _('Future Receipts')
                     if fields.get('qty_available'):
                         res['fields']['qty_available']['string'] = _('Received Qty')
 
@@ -511,80 +283,244 @@ class product_product(osv.osv):
         return res
 
 
+    def action_view_routes(self, cr, uid, ids, context=None):
+        template_obj = self.pool.get("product.template")
+        templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)]))
+        return template_obj.action_view_routes(cr, uid, templ_ids, context=context)
+
 class product_template(osv.osv):
     _name = 'product.template'
     _inherit = 'product.template'
+    
+    def _product_available(self, cr, uid, ids, name, arg, context=None):
+        res = dict.fromkeys(ids, 0)
+        for product in self.browse(cr, uid, ids, context=context):
+            res[product.id] = {
+                # "reception_count": sum([p.reception_count for p in product.product_variant_ids]),
+                # "delivery_count": sum([p.delivery_count for p in product.product_variant_ids]),
+                "qty_available": sum([p.qty_available for p in product.product_variant_ids]),
+                "virtual_available": sum([p.virtual_available for p in product.product_variant_ids]),
+                "incoming_qty": sum([p.incoming_qty for p in product.product_variant_ids]),
+                "outgoing_qty": sum([p.outgoing_qty for p in product.product_variant_ids]),
+            }
+        return res
+
+    def _search_product_quantity(self, cr, uid, obj, name, domain, context):
+        prod = self.pool.get("product.product")
+        res = []
+        for field, operator, value in domain:
+            #to prevent sql injections
+            assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
+            assert operator in ('<', '>', '=', '!=', '<=', '>='), 'Invalid domain operator'
+            assert isinstance(value, (float, int)), 'Invalid domain right operand'
+
+            if operator == '=':
+                operator = '=='
+
+            product_ids = prod.search(cr, uid, [], context=context)
+            ids = []
+            if product_ids:
+                #TODO: use a query instead of this browse record which is probably making the too much requests, but don't forget
+                #the context that can be set with a location, an owner...
+                for element in prod.browse(cr, uid, product_ids, context=context):
+                    if eval(str(element[field]) + operator + str(value)):
+                        ids.append(element.id)
+            res.append(('product_variant_ids', 'in', ids))
+        return res
+
+
+    def _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None):
+        res = {}
+        for product in self.browse(cr, uid, ids, context=context):
+            res[product.id] = str(product.qty_available) +  _(" On Hand")
+        return res
+
+
+
     _columns = {
+        'type': fields.selection([('product', 'Stockable Product'), ('consu', 'Consumable'), ('service', 'Service')], 'Product Type', required=True, help="Consumable: Will not imply stock management for this product. \nStockable product: Will imply stock management for this product."),
+        'qty_available2': fields.related('qty_available', type="float", relation="product.template", string="On Hand"),
         'property_stock_procurement': fields.property(
-            'stock.location',
             type='many2one',
             relation='stock.location',
             string="Procurement Location",
-            view_load=True,
             domain=[('usage','like','procurement')],
             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
         'property_stock_production': fields.property(
-            'stock.location',
             type='many2one',
             relation='stock.location',
             string="Production Location",
-            view_load=True,
             domain=[('usage','like','production')],
             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
         'property_stock_inventory': fields.property(
-            'stock.location',
             type='many2one',
             relation='stock.location',
             string="Inventory Location",
-            view_load=True,
             domain=[('usage','like','inventory')],
             help="This stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory."),
-        'property_stock_account_input': fields.property('account.account',
-            type='many2one', relation='account.account',
-            string='Stock Input Account', view_load=True,
-            help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
-                 "there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used."),
-        'property_stock_account_output': fields.property('account.account',
-            type='many2one', relation='account.account',
-            string='Stock Output Account', view_load=True,
-            help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
-                 "there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used."),
         'sale_delay': fields.float('Customer Lead Time', help="The average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
         'loc_rack': fields.char('Rack', size=16),
         'loc_row': fields.char('Row', size=16),
         'loc_case': fields.char('Case', size=16),
+        'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to specify a Serial Number for all moves containing this product and coming from a Supplier Location"),
+        'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Serial Number for all moves containing this product and going to a Customer Location"),
+        'track_all': fields.boolean('Full Lots Traceability', help="Forces to specify a Serial Number on each and every operation related to this product"),
+        
+        # sum of product variant qty
+        # 'reception_count': fields.function(_product_available, multi='qty_available',
+        #     fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
+        # 'delivery_count': fields.function(_product_available, multi='qty_available',
+        #     fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
+        'qty_available': fields.function(_product_available, multi='qty_available',
+            fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
+        'virtual_available': fields.function(_product_available, multi='qty_available',
+            fnct_search=_search_product_quantity, type='float', string='Quantity Available'),
+        'incoming_qty': fields.function(_product_available, multi='qty_available',
+            fnct_search=_search_product_quantity, type='float', string='Incoming'),
+        'outgoing_qty': fields.function(_product_available, multi='qty_available',
+            fnct_search=_search_product_quantity, type='float', string='Outgoing'),
+        
+        '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,..."),
     }
 
     _defaults = {
         'sale_delay': 7,
     }
 
-class product_category(osv.osv):
+    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.xmlid_to_res_id(cr, uid, 'stock.action_routes_form', raise_if_not_found=True)
+        result = act_obj.read(cr, uid, [result], context=context)[0]
+        result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]"
+        return result
+
+
+    def _get_products(self, cr, uid, ids, context=None):
+        products = []
+        for prodtmpl in self.browse(cr, uid, ids, context=None):
+            products += [x.id for x in prodtmpl.product_variant_ids]
+        return products
+    
+    def _get_act_window_dict(self, cr, uid, name, context=None):
+        mod_obj = self.pool.get('ir.model.data')
+        act_obj = self.pool.get('ir.actions.act_window')
+        result = mod_obj.xmlid_to_res_id(cr, uid, name, raise_if_not_found=True)
+        result = act_obj.read(cr, uid, [result], context=context)[0]
+        return result
+    
+    def action_open_quants(self, cr, uid, ids, context=None):
+        products = self._get_products(cr, uid, ids, context=context)
+        result = self._get_act_window_dict(cr, uid, 'stock.product_open_quants', context=context)
+        result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
+        result['context'] = "{'search_default_locationgroup': 1, 'search_default_internal_loc': 1}"
+        return result
+    
+    def action_view_orderpoints(self, cr, uid, ids, context=None):
+        products = self._get_products(cr, uid, ids, context=context)
+        result = self._get_act_window_dict(cr, uid, 'stock.product_open_orderpoint', context=context)
+        if len(ids) == 1 and len(products) == 1:
+            result['context'] = "{'default_product_id': " + str(products[0]) + ", 'search_default_product_id': " + str(products[0]) + "}"
+        else:
+            result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
+            result['context'] = "{}"
+        return result
+
+
+    def action_view_stock_moves(self, cr, uid, ids, context=None):
+        products = self._get_products(cr, uid, ids, context=context)
+        result = self._get_act_window_dict(cr, uid, 'stock.act_product_stock_move_open', context=context)
+        if len(ids) == 1 and len(products) == 1:
+            ctx = "{'tree_view_ref':'stock.view_move_tree', \
+                  'default_product_id': %s, 'search_default_product_id': %s}" \
+                  % (products[0], products[0])
+            result['context'] = ctx
+        else:
+            result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
+            result['context'] = "{'tree_view_ref':'stock.view_move_tree'}"
+        return result
+
+    def write(self, cr, uid, ids, vals, context=None):
+        if 'uom_po_id' in vals:
+            product_ids = self.pool.get('product.product').search(cr, uid, [('product_tmpl_id', 'in', ids)], context=context)
+            if self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', product_ids)], context=context, limit=1):
+                raise osv.except_osv(_('Error!'), _("You can not change the unit of measure of a product that has already been used in a stock move. If you need to change the unit of measure, you may deactivate this product.") % ())
+        return super(product_template, self).write(cr, uid, ids, vals, context=context)
+
+
+class product_removal_strategy(osv.osv):
+    _name = 'product.removal'
+    _description = 'Removal Strategy'
+
+    _columns = {
+        '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 = {
+        '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", copy=True),
+    }
+
+    _defaults = {
+        'method': 'fixed',
+    }
+
+    def putaway_apply(self, cr, uid, putaway_strat, product, context=None):
+        if putaway_strat.method == 'fixed':
+            for strat in putaway_strat.fixed_location_ids:
+                categ = product.categ_id
+                while categ:
+                    if strat.category_id.id == categ.id:
+                        return strat.fixed_location_id.id
+                    categ = categ.parent_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 = {}
+        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 = {
-        'property_stock_journal': fields.property('account.journal',
-            relation='account.journal', type='many2one',
-            string='Stock Journal', view_load=True,
-            help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
-        'property_stock_account_input_categ': fields.property('account.account',
-            type='many2one', relation='account.account',
-            string='Stock Input Account', view_load=True,
-            help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
-                 "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
-                 "can also directly be set on each product"),
-        'property_stock_account_output_categ': fields.property('account.account',
-            type='many2one', relation='account.account',
-            string='Stock Output Account', view_load=True,
-            help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
-                 "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
-                 "can also directly be set on each product"),
-        'property_stock_valuation_account_id': fields.property('account.account',
-            type='many2one',
-            relation='account.account',
-            string="Stock Valuation Account",
-            view_load=True,
-            help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
+        'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"),
+        '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),
     }