@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."),
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()
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
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,
'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)
'picking_id' : new_picking,
'state': 'assigned',
'move_dest_id': False,
- 'price_unit': move.price_unit,
}
prodlot_id = prodlot_ids[move.id]
if prodlot_id:
}
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):
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:
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)
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
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: