[MERGE] merge with latest stable
[odoo/odoo.git] / addons / stock / stock.py
index 454ca17..49785fd 100644 (file)
@@ -111,46 +111,42 @@ class stock_location(osv.osv):
         @param field_names: Name of field
         @return: Dictionary of values
         """
-        prod_id = context and context.get('product_id', False)
-
-        product_product_obj = self.pool.get('product.product')
-
-        cr.execute('select distinct product_id, location_id from stock_move where location_id in %s', (tuple(ids), ))
-        dict1 = cr.dictfetchall()
-        cr.execute('select distinct product_id, location_dest_id as location_id from stock_move where location_dest_id in %s', (tuple(ids), ))
-        dict2 = cr.dictfetchall()
-        res_products_by_location = sorted(dict1+dict2, key=itemgetter('location_id'))
-        products_by_location = dict((k, [v['product_id'] for v in itr]) for k, itr in groupby(res_products_by_location, itemgetter('location_id')))
-
-        result = dict([(i, {}.fromkeys(field_names, 0.0)) for i in ids])
-        result.update(dict([(i, {}.fromkeys(field_names, 0.0)) for i in list(set([aaa['location_id'] for aaa in res_products_by_location]))]))
-
+        if not context:
+            context = {}
+        
+        #Find currency
         currency_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
         currency_obj = self.pool.get('res.currency')
         currency = currency_obj.browse(cr, uid, currency_id, context=context)
-        for loc_id, product_ids in products_by_location.items():
-            if prod_id:
-                product_ids = [prod_id]
-            c = (context or {}).copy()
+        #Find list of product
+        prod_id = context.get('product_id', False)
+        if prod_id:
+            product_ids = [prod_id]
+        else:
+            cr.execute('select distinct product_id from stock_move where location_id in %s', (tuple(ids), ))
+            moves = cr.dictfetchall()
+            product_ids = [move['product_id'] for move in moves]
+            
+        #Compute result for all location
+        result = dict([(i, dict.fromkeys(field_names, 0.0)) for i in ids])
+        for loc_id in ids:
+            c = context.copy()
             c['location'] = loc_id
-            for prod in product_product_obj.browse(cr, uid, product_ids, context=c):
-                for f in field_names:
-                    if f == 'stock_real':
-                        if loc_id not in result:
-                            result[loc_id] = {}
-                        result[loc_id][f] += prod.qty_available
-                    elif f == 'stock_virtual':
-                        result[loc_id][f] += prod.virtual_available
-                    elif f == 'stock_real_value':
-                        amount = prod.qty_available * prod.standard_price
-                        amount = currency_obj.round(cr, uid, currency, amount)
-                        result[loc_id][f] += amount
-                    elif f == 'stock_virtual_value':
-                        amount = prod.virtual_available * prod.standard_price
-                        amount = currency_obj.round(cr, uid, currency, amount)
-                        result[loc_id][f] += amount
+            for prod in self.pool.get('product.product').browse(cr, uid, product_ids, context=c):
+                if 'stock_real' in field_names:
+                    result[loc_id]['stock_real'] += prod.qty_available
+                elif 'stock_virtual' in field_names:
+                    result[loc_id]['stock_virtual'] += prod.virtual_available
+                elif 'stock_real_value' in field_names:
+                    amount = prod.qty_available * prod.standard_price
+                    amount = currency_obj.round(cr, uid, currency, amount)
+                    result[loc_id]['stock_real_value'] += amount
+                elif 'stock_virtual_value' in field_names:
+                    amount = prod.virtual_available * prod.standard_price
+                    amount = currency_obj.round(cr, uid, currency, amount)
+                    result[loc_id]['stock_virtual_value'] += amount
         return result
-
+    
     _columns = {
         'name': fields.char('Location Name', size=64, required=True, translate=True),
         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
@@ -359,107 +355,118 @@ class stock_location(osv.osv):
     def _product_virtual_get(self, cr, uid, id, product_ids=False, context=None, states=['done']):
         return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed', 'waiting', 'assigned', 'done'])
 
+    def _try_lock_product_reserve(self, cr, uid, location_ids, product_id, product_qty, context=None):
+        try:
+            # Must lock with a separate select query than the ones used in _product_reserve
+            # because FOR UPDATE can't be used with aggregation/group by's
+            # (i.e. when individual rows aren't identifiable).
+            # We use a SAVEPOINT to be able to rollback this part of the transaction without
+            # failing the whole transaction in case the LOCK cannot be acquired.
+            cr.execute("SAVEPOINT stock_location_product_reserve")
+            # We lock all stock moves in states we are going to consider in the
+            # calculation. By locking all DONE move we prevent other transactions
+            # from reserving the same products, as they won't be allowed to SELECT
+            # them until we're done.
+            cr.execute("""SELECT id FROM stock_move
+                          WHERE product_id=%s
+                          AND (
+                                (location_dest_id IN %s AND state = 'done')
+                                OR
+                                (location_id IN %s AND state in ('done', 'assigned'))
+                               )
+                          FOR UPDATE of stock_move NOWAIT""",
+                       (product_id, location_ids, location_ids), log_exceptions=False)
+        except Exception:
+            # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
+            # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
+            # state, we return False as if the products were not available, and log it:
+            cr.execute("ROLLBACK TO stock_location_product_reserve")
+            logger = logging.getLogger('stock.location')
+            logger.warn("Failed attempt to reserve %s x product %s, likely due to another transaction already in progress. Next attempt is likely to work. Detailed error available at DEBUG level.", product_qty, product_id)
+            logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
+            return False
+        return True
+
     def _product_reserve(self, cr, uid, ids, product_id, product_qty, context=None, lock=False):
         """
         Attempt to find a quantity ``product_qty`` (in the product's default uom or the uom passed in ``context``) of product ``product_id``
         in locations with id ``ids`` and their child locations. If ``lock`` is True, the stock.move lines
         of product with id ``product_id`` in the searched location will be write-locked using Postgres's
-        "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reservin
+        "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reserving
         twice the same products.
         If ``lock`` is True and the lock cannot be obtained (because another transaction has locked some of
         the same stock.move lines), a log line will be output and False will be returned, as if there was
         not enough stock.
 
         :param product_id: Id of product to reserve
-        :param product_qty: Quantity of product to reserve (in the product's default uom or the uom passed in ``context``)
+        :param product_qty: Quantity of product to reserve (in the uom passed in ``context``)
         :param lock: if True, the stock.move lines of product with id ``product_id`` in all locations (and children locations) with ``ids`` will
                      be write-locked using postgres's "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back. This is
                      to prevent reserving twice the same products.
-        :param context: optional context dictionary: it a 'uom' key is present it will be used instead of the default product uom to
-                        compute the ``product_qty`` and in the return value.
-        :return: List of tuples in the form (qty, location_id) with the (partial) quantities that can be taken in each location to
-                 reach the requested product_qty (``qty`` is expressed in the default uom of the product), of False if enough
-                 products could not be found, or the lock could not be obtained (and ``lock`` was True).
+        :param context: context dictionary with 'uom' key mapped to the ID of the UoM to use to compute the product quantities
+        :return: List of pairs (qty, location_id) with the (partial) quantities that can be taken in each location to
+                 reach the requested product_qty (expressed in the requested uom), or False if not enough
+                 products could be found, or the lock could not be obtained (and ``lock`` was True).
+                 sum(qty) == ``product_qty``.
         """
-        result = []
-        amount = 0.0
         if context is None:
             context = {}
-        pool_uom = self.pool.get('product.uom')
-        for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
-            if lock:
-                try:
-                    # Must lock with a separate select query because FOR UPDATE can't be used with
-                    # aggregation/group by's (when individual rows aren't identifiable).
-                    # We use a SAVEPOINT to be able to rollback this part of the transaction without
-                    # failing the whole transaction in case the LOCK cannot be acquired.
-                    cr.execute("SAVEPOINT stock_location_product_reserve")
-                    cr.execute("""SELECT id FROM stock_move
-                                  WHERE product_id=%s AND
-                                          (
-                                            (location_dest_id=%s AND
-                                             location_id<>%s AND
-                                             state='done')
-                                            OR
-                                            (location_id=%s AND
-                                             location_dest_id<>%s AND
-                                             state in ('done', 'assigned'))
-                                          )
-                                  FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id), log_exceptions=False)
-                except Exception:
-                    # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
-                    # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
-                    # state, we return False as if the products were not available, and log it:
-                    cr.execute("ROLLBACK TO stock_location_product_reserve")
-                    logger = logging.getLogger('stock.location')
-                    logger.warn("Failed attempt to reserve %s x product %s, likely due to another transaction already in progress. Next attempt is likely to work. Detailed error available at DEBUG level.", product_qty, product_id)
-                    logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
-                    return False
+        location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
+        locations_tuple = tuple(location_ids)
+        if lock and not self._try_lock_product_reserve(cr, uid, locations_tuple, product_id, product_qty, context=context):
+            return False
 
-            # XXX TODO: rewrite this with one single query, possibly even the quantity conversion
-            cr.execute("""SELECT product_uom, sum(product_qty) AS product_qty
-                          FROM stock_move
-                          WHERE location_dest_id=%s AND
-                                location_id<>%s AND
-                                product_id=%s AND
-                                state='done'
-                          GROUP BY product_uom
-                       """,
-                       (id, id, product_id))
-            results = cr.dictfetchall()
-            cr.execute("""SELECT product_uom,-sum(product_qty) AS product_qty
-                          FROM stock_move
-                          WHERE location_id=%s AND
-                                location_dest_id<>%s AND
-                                product_id=%s AND
-                                state in ('done', 'assigned')
-                          GROUP BY product_uom
-                       """,
-                       (id, id, product_id))
-            results += cr.dictfetchall()
-
-            total = 0.0
-            results2 = 0.0
-
-            for r in results:
-                amount = pool_uom._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
-                results2 += amount
-                total += amount
-
-            if total <= 0.0:
+        # Giant query to obtain triplets of (product_uom, product_qty, location_id) summing all relevant
+        # stock moves quantities per location,  with incoming quantities taken positive,
+        # and outgoing taken negative.
+        cr.execute("""SELECT x.product_uom, SUM(x.coeff * x.product_qty) as product_qty, x.loc_id as location_id
+                      FROM (
+                          SELECT 1.0 as coeff, product_uom, location_dest_id as loc_id,
+                                 sum(product_qty) AS product_qty
+                              FROM stock_move
+                              WHERE location_dest_id in %s AND
+                                    location_id != location_dest_id AND
+                                    product_id = %s AND
+                                    state = 'done'
+                              GROUP BY location_dest_id, product_uom
+                          UNION
+                          SELECT -1.0 as coeff, product_uom, location_id as loc_id,
+                                 sum(product_qty) AS product_qty
+                              FROM stock_move
+                              WHERE location_id in %s AND
+                                    location_id != location_dest_id AND
+                                    product_id = %s AND
+                                    state in ('done', 'assigned')
+                              GROUP BY location_id, product_uom
+                      ) AS x
+                      GROUP BY x.loc_id, x.product_uom
+                   """,
+                   (locations_tuple, product_id, locations_tuple, product_id))
+        sum_rows = cr.fetchall()
+
+        qty_by_location = {}
+        ProductUom = self.pool.get('product.uom')
+        target_uom = context.get('uom')
+        # Convert all UoMs into target UoM
+        for uom_id, qty, loc_id in sum_rows:
+            qty_by_location.setdefault(loc_id,0.0)
+            qty_by_location[loc_id] += ProductUom._compute_qty(cr, uid, uom_id, qty, target_uom)
+
+        # to compute final result we handle locations in the
+        # order in which they were returned by the original search().
+        result = []
+        for loc_id in location_ids:
+            if loc_id not in qty_by_location:
+                #skip location without this product
                 continue
-
-            amount = results2
-            if amount > 0:
-                if amount > min(total, product_qty):
-                    amount = min(product_qty, total)
-                result.append((amount, id))
-                product_qty -= amount
-                total -= amount
-                if product_qty <= 0.0:
-                    return result
-                if total <= 0.0:
-                    continue
+            qty = qty_by_location[loc_id]
+            if qty <= 0.0:
+                continue
+            qty = min(product_qty, qty)
+            result.append((qty, loc_id))
+            product_qty -= qty
+            if product_qty <= 0.0:
+                return result
         return False
 
 stock_location()
@@ -1036,6 +1043,9 @@ class stock_picking(osv.osv):
             for move_line in picking.move_lines:
                 if move_line.state == 'cancel':
                     continue
+                if move_line.scrapped:
+                    # do no invoice scrapped products
+                    continue
                 origin = move_line.picking_id.name or ''
                 if move_line.picking_id.origin:
                     origin += ':' + move_line.picking_id.origin
@@ -1067,8 +1077,9 @@ class stock_picking(osv.osv):
                 uos_id = move_line.product_uos and move_line.product_uos.id or False
                 if not uos_id and inv_type in ('out_invoice', 'out_refund'):
                     uos_id = move_line.product_uom.id
-
                 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
+                if move_line.price_unit != 0 and price_unit != move_line.price_unit:
+                    price_unit = move_line.price_unit
                 invoice_line_id = invoice_line_obj.create(cr, uid, {
                     'name': name,
                     'origin': origin,
@@ -1081,6 +1092,7 @@ class stock_picking(osv.osv):
                     'quantity': move_line.product_uos_qty or move_line.product_qty,
                     'invoice_line_tax_id': [(6, 0, tax_ids)],
                     'account_analytic_id': account_analytic_id,
+                    'note': move_line.note
                 }, context=context)
                 self._invoice_line_hook(cr, uid, move_line, invoice_line_id)
 
@@ -1243,7 +1255,6 @@ class stock_picking(osv.osv):
                             'picking_id' : new_picking,
                             'state': 'assigned',
                             'move_dest_id': False,
-                            'price_unit': move.price_unit,
                     }
                     prodlot_id = prodlot_ids[move.id]
                     if prodlot_id:
@@ -1621,6 +1632,8 @@ class stock_move(osv.osv):
     }
 
     def write(self, cr, uid, ids, vals, context=None):
+        if isinstance(ids, (int, long)):
+            ids = [ids]
         if uid != 1:
             frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
             for move in self.browse(cr, uid, ids, context=context):
@@ -1834,7 +1847,7 @@ class stock_move(osv.osv):
                 old_ptype = location_obj.picking_type_get(cr, uid, picking.move_lines[0].location_id, picking.move_lines[0].location_dest_id)
                 if old_ptype != picking.type:
                     old_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + old_ptype)
-                    picking_obj.write(cr, uid, [picking.id], {'name': old_pick_name}, context=context)
+                    picking_obj.write(cr, uid, [picking.id], {'name': old_pick_name, 'type': old_ptype}, context=context)
             else:
                 pickid = False
             for move, (loc, dummy, delay, dummy, company_id, ptype) in todo:
@@ -1925,11 +1938,11 @@ class stock_move(osv.osv):
                     done.append(move.id)
                     pickings[move.picking_id.id] = 1
                     r = res.pop(0)
-                    cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
+                    cr.execute('update stock_move set location_id=%s, product_qty=%s, product_uos_qty=%s where id=%s', (r[1], r[0], r[0] * move.product_id.uos_coeff, move.id))
 
                     while res:
                         r = res.pop(0)
-                        move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
+                        move_id = self.copy(cr, uid, move.id, {'product_qty': r[0],'product_uos_qty': r[0] * move.product_id.uos_coeff,'location_id': r[1]})
                         done.append(move_id)
         if done:
             count += len(done)
@@ -2057,7 +2070,7 @@ class stock_move(osv.osv):
                 context = {}
             currency_ctx = dict(context, currency_id = move.company_id.currency_id.id)
             amount_unit = move.product_id.price_get('standard_price', currency_ctx)[move.product_id.id]
-            reference_amount = amount_unit * qty or 1.0
+            reference_amount = amount_unit * qty
 
         return reference_amount, reference_currency_id
 
@@ -2133,7 +2146,7 @@ class stock_move(osv.osv):
                 self.write(cr, uid, [move.id], {'move_history_ids': [(4, move.move_dest_id.id)]})
                 #cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
                 if move.move_dest_id.state in ('waiting', 'confirmed'):
-                    if move.prodlot_id.id and move.product_id==move.move_dest_id.product_id:
+                    if move.prodlot_id.id and move.product_id.id == move.move_dest_id.product_id.id:
                         self.write(cr, uid, [move.move_dest_id.id], {'prodlot_id':move.prodlot_id.id})
                     self.force_assign(cr, uid, [move.move_dest_id.id], context=context)
                     if move.move_dest_id.picking_id: