Merge lp:~openerp-dev/openobject-addons/6.0-bug-770243-odo into lp:openobject-addons/6.0

Proposed by Olivier Dony (Odoo)
Status: Merged
Merged at revision: 4753
Proposed branch: lp:~openerp-dev/openobject-addons/6.0-bug-770243-odo
Merge into: lp:openobject-addons/6.0
Diff against target: 206 lines (+96/-84)
1 file modified
stock/stock.py (+96/-84)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/6.0-bug-770243-odo
Reviewer Review Type Date Requested Status
OpenERP Publisher's Warranty Team Pending
Review via email: mp+63116@code.launchpad.net

Description of the change

This branch fixes the performance issues that happen during reservation of stock.moves for databases with a large number of stock locations.

The locking and reservation system have been streamlined and are a lot more efficient in terms of iterations and database queries.

Support team should review and merge this for OPW.

Thanks!

To post a comment you must log in.
Revision history for this message
Jay Vora (Serpent Consulting Services) (jayvora) wrote :

Olivier,

Thanks for proposing merge for one of most awaited bugs.

Sorry to say, but somehow the branch contains some conflicts.

Can you recheck?

Thanks.

Revision history for this message
Jay Vora (Serpent Consulting Services) (jayvora) wrote :

Interestingly,

The diff of the merge and the commit differ heavily.
http://bazaar.launchpad.net/~openerp-dev/openobject-addons/6.0-bug-770243-odo/revision/4574

I guess its a bug or I am missing something!?!

Thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'stock/stock.py'
2--- stock/stock.py 2011-07-25 09:53:22 +0000
3+++ stock/stock.py 2011-07-27 11:21:46 +0000
4@@ -359,106 +359,118 @@
5 def _product_virtual_get(self, cr, uid, id, product_ids=False, context=None, states=['done']):
6 return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed', 'waiting', 'assigned', 'done'])
7
8+ def _try_lock_product_reserve(self, cr, uid, location_ids, product_id, product_qty, context=None):
9+ try:
10+ # Must lock with a separate select query than the ones used in _product_reserve
11+ # because FOR UPDATE can't be used with aggregation/group by's
12+ # (i.e. when individual rows aren't identifiable).
13+ # We use a SAVEPOINT to be able to rollback this part of the transaction without
14+ # failing the whole transaction in case the LOCK cannot be acquired.
15+ cr.execute("SAVEPOINT stock_location_product_reserve")
16+ # We lock all stock moves in states we are going to consider in the
17+ # calculation. By locking all DONE move we prevent other transactions
18+ # from reserving the same products, as they won't be allowed to SELECT
19+ # them until we're done.
20+ cr.execute("""SELECT id FROM stock_move
21+ WHERE product_id=%s
22+ AND (
23+ (location_dest_id IN %s AND state = 'done')
24+ OR
25+ (location_id IN %s AND state in ('done', 'assigned'))
26+ )
27+ FOR UPDATE of stock_move NOWAIT""",
28+ (product_id, location_ids, location_ids), log_exceptions=False)
29+ except Exception:
30+ # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
31+ # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
32+ # state, we return False as if the products were not available, and log it:
33+ cr.execute("ROLLBACK TO stock_location_product_reserve")
34+ logger = logging.getLogger('stock.location')
35+ 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)
36+ logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
37+ return False
38+ return True
39+
40 def _product_reserve(self, cr, uid, ids, product_id, product_qty, context=None, lock=False):
41 """
42 Attempt to find a quantity ``product_qty`` (in the product's default uom or the uom passed in ``context``) of product ``product_id``
43 in locations with id ``ids`` and their child locations. If ``lock`` is True, the stock.move lines
44 of product with id ``product_id`` in the searched location will be write-locked using Postgres's
45- "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reservin
46+ "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reserving
47 twice the same products.
48 If ``lock`` is True and the lock cannot be obtained (because another transaction has locked some of
49 the same stock.move lines), a log line will be output and False will be returned, as if there was
50 not enough stock.
51
52 :param product_id: Id of product to reserve
53- :param product_qty: Quantity of product to reserve (in the product's default uom or the uom passed in ``context``)
54+ :param product_qty: Quantity of product to reserve (in the uom passed in ``context``)
55 :param lock: if True, the stock.move lines of product with id ``product_id`` in all locations (and children locations) with ``ids`` will
56 be write-locked using postgres's "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back. This is
57 to prevent reserving twice the same products.
58- :param context: optional context dictionary: it a 'uom' key is present it will be used instead of the default product uom to
59- compute the ``product_qty`` and in the return value.
60- :return: List of tuples in the form (qty, location_id) with the (partial) quantities that can be taken in each location to
61- reach the requested product_qty (``qty`` is expressed in the default uom of the product), of False if enough
62- products could not be found, or the lock could not be obtained (and ``lock`` was True).
63+ :param context: context dictionary with 'uom' key mapped to the ID of the UoM to use to compute the product quantities
64+ :return: List of pairs (qty, location_id) with the (partial) quantities that can be taken in each location to
65+ reach the requested product_qty (expressed in the requested uom), or False if not enough
66+ products could be found, or the lock could not be obtained (and ``lock`` was True).
67+ sum(qty) == ``product_qty``.
68 """
69+ if context is None:
70+ context = {}
71+ location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
72+ locations_tuple = tuple(location_ids)
73+ if lock and not self._try_lock_product_reserve(cr, uid, locations_tuple, product_id, product_qty, context=context):
74+ return False
75+
76+ # Giant query to obtain triplets of (product_uom, product_qty, location_id) summing all relevant
77+ # stock moves quantities per location, with incoming quantities taken positive,
78+ # and outgoing taken negative.
79+ cr.execute("""SELECT x.product_uom, SUM(x.coeff * x.product_qty) as product_qty, x.loc_id as location_id
80+ FROM (
81+ SELECT 1.0 as coeff, product_uom, location_dest_id as loc_id,
82+ sum(product_qty) AS product_qty
83+ FROM stock_move
84+ WHERE location_dest_id in %s AND
85+ location_id != location_dest_id AND
86+ product_id = %s AND
87+ state = 'done'
88+ GROUP BY location_dest_id, product_uom
89+ UNION
90+ SELECT -1.0 as coeff, product_uom, location_id as loc_id,
91+ sum(product_qty) AS product_qty
92+ FROM stock_move
93+ WHERE location_id in %s AND
94+ location_id != location_dest_id AND
95+ product_id = %s AND
96+ state in ('done', 'assigned')
97+ GROUP BY location_id, product_uom
98+ ) AS x
99+ GROUP BY x.loc_id, x.product_uom
100+ """,
101+ (locations_tuple, product_id, locations_tuple, product_id))
102+ sum_rows = cr.fetchall()
103+
104+ qty_by_location = {}
105+ ProductUom = self.pool.get('product.uom')
106+ target_uom = context.get('uom')
107+ # Convert all UoMs into target UoM
108+ for uom_id, qty, loc_id in sum_rows:
109+ qty_by_location.setdefault(loc_id,0.0)
110+ qty_by_location[loc_id] += ProductUom._compute_qty(cr, uid, uom_id, qty, target_uom)
111+
112+ # to compute final result we handle locations in the
113+ # order in which they were returned by the original search().
114 result = []
115- amount = 0.0
116- if context is None:
117- context = {}
118- pool_uom = self.pool.get('product.uom')
119- for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
120- if lock:
121- try:
122- # Must lock with a separate select query because FOR UPDATE can't be used with
123- # aggregation/group by's (when individual rows aren't identifiable).
124- # We use a SAVEPOINT to be able to rollback this part of the transaction without
125- # failing the whole transaction in case the LOCK cannot be acquired.
126- cr.execute("SAVEPOINT stock_location_product_reserve")
127- cr.execute("""SELECT id FROM stock_move
128- WHERE product_id=%s AND
129- (
130- (location_dest_id=%s AND
131- location_id<>%s AND
132- state='done')
133- OR
134- (location_id=%s AND
135- location_dest_id<>%s AND
136- state in ('done', 'assigned'))
137- )
138- FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id), log_exceptions=False)
139- except Exception:
140- # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
141- # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
142- # state, we return False as if the products were not available, and log it:
143- cr.execute("ROLLBACK TO stock_location_product_reserve")
144- logger = logging.getLogger('stock.location')
145- 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)
146- logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
147- return False
148-
149- # XXX TODO: rewrite this with one single query, possibly even the quantity conversion
150- cr.execute("""SELECT product_uom, sum(product_qty) AS product_qty
151- FROM stock_move
152- WHERE location_dest_id=%s AND
153- location_id<>%s AND
154- product_id=%s AND
155- state='done'
156- GROUP BY product_uom
157- """,
158- (id, id, product_id))
159- results = cr.dictfetchall()
160- cr.execute("""SELECT product_uom,-sum(product_qty) AS product_qty
161- FROM stock_move
162- WHERE location_id=%s AND
163- location_dest_id<>%s AND
164- product_id=%s AND
165- state in ('done', 'assigned')
166- GROUP BY product_uom
167- """,
168- (id, id, product_id))
169- results += cr.dictfetchall()
170- total = 0.0
171- results2 = 0.0
172-
173- for r in results:
174- amount = pool_uom._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
175- results2 += amount
176- total += amount
177-
178- if total <= 0.0:
179- continue
180-
181- amount = results2
182- if amount > 0:
183- if amount > min(total, product_qty):
184- amount = min(product_qty, total)
185- result.append((amount, id))
186- product_qty -= amount
187- total -= amount
188- if product_qty <= 0.0:
189- return result
190- if total <= 0.0:
191- continue
192+ for loc_id in location_ids:
193+ if loc_id not in qty_by_location:
194+ #skip location without this product
195+ continue
196+ qty = qty_by_location[loc_id]
197+ if qty <= 0.0:
198+ continue
199+ qty = min(product_qty, qty)
200+ result.append((qty, loc_id))
201+ product_qty -= qty
202+ if product_qty <= 0.0:
203+ return result
204 return False
205
206 stock_location()