Merge lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale into lp:stock-logistic-warehouse

Proposed by Lionel Sausin - Initiatives/Numérigraphe
Status: Rejected
Rejected by: Pedro Manuel Baeza
Proposed branch: lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale
Merge into: lp:stock-logistic-warehouse
Prerequisite: lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available
Diff against target: 492 lines (+441/-0)
8 files modified
stock_available/res_config.py (+7/-0)
stock_available/res_config_view.xml (+4/-0)
stock_available_sale/__init__.py (+22/-0)
stock_available_sale/__openerp__.py (+48/-0)
stock_available_sale/product.py (+232/-0)
stock_available_sale/product_view.xml (+19/-0)
stock_available_sale/sale_stock.py (+43/-0)
stock_available_sale/test/quoted_qty.yml (+66/-0)
To merge this branch: bzr merge lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale
Reviewer Review Type Date Requested Status
Alexandre Fayolle - camptocamp Needs Resubmitting
Review via email: mp+220761@code.launchpad.net

Description of the change

Add stock_available_sale: take sale quotations into account in the stock quantity available to promise" stock_available stock_available_sale.

This branch builds upon lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available, which adds a generic module to compute the stock available to promise in a configurable way.

The new module uses a context trick introduced in the previous branch, to make the Sale module warn salesman over insufficient quantity available to promise instead of insufficient virtual stock, with a very low code impact.

Module Co-authored by Loïc Bellier and your humble servant.

To post a comment you must log in.
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

The source code management for this project has been moved to https://github.com/OCA/stock-logistics-warehouse

Could you resubmit this MP on the new site?

review: Needs Resubmitting
Revision history for this message
Lionel Sausin - Initiatives/Numérigraphe (ls-initiatives) wrote :

Unmerged revisions

36. By Numérigraphe

[ADD] stock_available_sale: take sale quotations into account in the stock quantity available to promise

35. By Numérigraphe

[ADD] stock_available: generic module to compute the stock quantity available to promise using several implementations. Make stock_available_immediatly the first configurable implementation

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'stock_available/res_config.py'
2--- stock_available/res_config.py 2014-05-23 07:53:49 +0000
3+++ stock_available/res_config.py 2014-05-23 07:53:49 +0000
4@@ -31,4 +31,11 @@
5 help="This will subtract incoming quantities from the quantities"
6 "available to promise.\n"
7 "This installs the module stock_available_immediately."),
8+ 'module_stock_available_sale': fields.boolean(
9+ 'Exclude goods already in sale quotations',
10+ help="This will subtract quantities from the sale quotations from"
11+ "the quantities available to promise.\n"
12+ "This installs the modules stock_available_sale.\n"
13+ "If the modules sale and sale_delivery_date are not "
14+ "installed, this will install them too"),
15 }
16
17=== modified file 'stock_available/res_config_view.xml'
18--- stock_available/res_config_view.xml 2014-05-23 07:53:49 +0000
19+++ stock_available/res_config_view.xml 2014-05-23 07:53:49 +0000
20@@ -15,6 +15,10 @@
21 <field name="module_stock_available_immediately" class="oe_inline" />
22 <label for="module_stock_available_immediately" />
23 </div>
24+ <div>
25+ <field name="module_stock_available_sale" class="oe_inline" />
26+ <label for="module_stock_available_sale" />
27+ </div>
28 </div>
29 </group>
30 </xpath>
31
32=== added directory 'stock_available_sale'
33=== added file 'stock_available_sale/__init__.py'
34--- stock_available_sale/__init__.py 1970-01-01 00:00:00 +0000
35+++ stock_available_sale/__init__.py 2014-05-23 07:53:49 +0000
36@@ -0,0 +1,22 @@
37+# -*- coding: utf-8 -*-
38+##############################################################################
39+#
40+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
41+#
42+# This program is free software: you can redistribute it and/or modify
43+# it under the terms of the GNU Affero General Public License as
44+# published by the Free Software Foundation, either version 3 of the
45+# License, or (at your option) any later version.
46+#
47+# This program is distributed in the hope that it will be useful,
48+# but WITHOUT ANY WARRANTY; without even the implied warranty of
49+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
50+# GNU Affero General Public License for more details.
51+#
52+# You should have received a copy of the GNU Affero General Public License
53+# along with this program. If not, see <http://www.gnu.org/licenses/>.
54+#
55+##############################################################################
56+
57+from . import product
58+from . import sale_stock
59
60=== added file 'stock_available_sale/__openerp__.py'
61--- stock_available_sale/__openerp__.py 1970-01-01 00:00:00 +0000
62+++ stock_available_sale/__openerp__.py 2014-05-23 07:53:49 +0000
63@@ -0,0 +1,48 @@
64+# -*- coding: utf-8 -*-
65+##############################################################################
66+#
67+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
68+#
69+# This program is free software: you can redistribute it and/or modify
70+# it under the terms of the GNU Affero General Public License as
71+# published by the Free Software Foundation, either version 3 of the
72+# License, or (at your option) any later version.
73+#
74+# This program is distributed in the hope that it will be useful,
75+# but WITHOUT ANY WARRANTY; without even the implied warranty of
76+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
77+# GNU Affero General Public License for more details.
78+#
79+# You should have received a copy of the GNU Affero General Public License
80+# along with this program. If not, see <http://www.gnu.org/licenses/>.
81+#
82+##############################################################################
83+
84+{
85+ 'name': 'Quotations in quantity available to promise',
86+ 'version': '2.0',
87+ 'author': u'Numérigraphe SÀRL',
88+ 'category': 'Hidden',
89+ 'depends': [
90+ 'stock_available',
91+ 'sale_order_dates',
92+ 'sale_stock',
93+ ],
94+ 'description': """
95+This module computes the quoted quantity of the Products, and subtracts it from
96+the quantities available to promise .
97+
98+"Quoted" is defined as the sum of the quantities of this product in Quotations,
99+taking the context's shop or warehouse into account.
100+
101+When entering sale orders, the salesperson get warned if the quantity available
102+to promise is insufficient (instead when the virtual stock is
103+insufficient).""",
104+ 'data': [
105+ 'product_view.xml',
106+ ],
107+ 'test': [
108+ 'test/quoted_qty.yml',
109+ ],
110+ 'license': 'AGPL-3',
111+}
112
113=== added file 'stock_available_sale/product.py'
114--- stock_available_sale/product.py 1970-01-01 00:00:00 +0000
115+++ stock_available_sale/product.py 2014-05-23 07:53:49 +0000
116@@ -0,0 +1,232 @@
117+# -*- coding: utf-8 -*-
118+##############################################################################
119+#
120+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
121+#
122+# This program is free software: you can redistribute it and/or modify
123+# it under the terms of the GNU Affero General Public License as
124+# published by the Free Software Foundation, either version 3 of the
125+# License, or (at your option) any later version.
126+#
127+# This program is distributed in the hope that it will be useful,
128+# but WITHOUT ANY WARRANTY; without even the implied warranty of
129+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
130+# GNU Affero General Public License for more details.
131+#
132+# You should have received a copy of the GNU Affero General Public License
133+# along with this program. If not, see <http://www.gnu.org/licenses/>.
134+#
135+##############################################################################
136+
137+from openerp.osv import orm, fields
138+import decimal_precision as dp
139+
140+# Function which uses the pool to call the method from the other modules too.
141+from openerp.addons.stock_available import _product_available_fnct
142+
143+
144+class ProductProduct(orm.Model):
145+ """Add the computation for the stock available to promise"""
146+ _inherit = 'product.product'
147+
148+ def _product_available(self, cr, uid, ids, field_names=None, arg=False,
149+ context=None):
150+ """Compute the quantities in Quotations."""
151+ # Check the context
152+ if context is None:
153+ context = {}
154+ # Prepare an alternative context without 'uom', to avoid cross-category
155+ # conversions when reading the available stock of components
156+ if 'uom' in context:
157+ context_wo_uom = context.copy()
158+ del context_wo_uom['uom']
159+ else:
160+ context_wo_uom = context
161+
162+ if field_names is None:
163+ field_names = []
164+
165+ # Compute the core quantities
166+ res = super(ProductProduct, self)._product_available(
167+ cr, uid, ids, field_names=field_names, arg=arg, context=context)
168+
169+ # Compute the quantities quoted/available to promise
170+ if ('quoted_qty' in field_names
171+ or 'immediately_usable_qty' in field_names):
172+ date_str, date_args = self._get_dates(cr, uid, ids,
173+ context=context)
174+
175+ # Limit the search to some shops according to the context
176+ shop_str, shop_args = self._get_shops(cr, uid, ids,
177+ context=context)
178+
179+ # Query the total by Product and UoM
180+ cr.execute(
181+ """
182+ SELECT sum(product_uom_qty), product_id, product_uom
183+ FROM sale_order_line
184+ INNER JOIN sale_order
185+ ON (sale_order_line.order_id = sale_order.id)
186+ WHERE product_id in %s
187+ AND sale_order_line.state = 'draft' """
188+ + date_str + shop_str +
189+ "GROUP BY sale_order_line.product_id, product_uom",
190+ (tuple(ids),) + date_args + shop_args)
191+ results = cr.fetchall()
192+
193+ # Get the UoM resources we'll need for conversion
194+ # UoMs from the products
195+ uoms_o = {}
196+ product2uom = {}
197+ for product in self.browse(cr, uid, ids, context=context):
198+ product2uom[product.id] = product.uom_id
199+ uoms_o[product.uom_id.id] = product.uom_id
200+ # UoM from the results and the context
201+ uom_obj = self.pool['product.uom']
202+ uoms = map(lambda stock_product_uom_qty: stock_product_uom_qty[2],
203+ results)
204+ if context.get('uom', False):
205+ uoms.append(context['uom'])
206+ uoms = filter(lambda stock_product_uom_qty:
207+ stock_product_uom_qty not in uoms_o.keys(), uoms)
208+ if uoms:
209+ uoms = uom_obj.browse(cr, uid, list(set(uoms)),
210+ context=context)
211+ for o in uoms:
212+ uoms_o[o.id] = o
213+
214+ # Compute the quoted quantity
215+ for (amount, prod_id, prod_uom) in results:
216+ # Convert the amount to the product's UoM without rounding
217+ amount = amount / uoms_o[prod_uom].factor
218+ if ('quoted_qty' in field_names):
219+ res[prod_id]['quoted_qty'] -= amount
220+ if ('immediately_usable_qty' in field_names):
221+ res[prod_id]['immediately_usable_qty'] -= amount
222+
223+ # Round and optionally convert the results to the requested UoM
224+ for prod_id, stock_qty in res.iteritems():
225+ if context.get('uom', False):
226+ # Convert to the requested UoM
227+ res_uom = uoms_o[context['uom']]
228+ else:
229+ # The conversion is unneeded but we do need the rounding
230+ res_uom = product2uom[prod_id]
231+ if ('quoted_qty' in field_names):
232+ stock_qty['quoted_qty'] = uom_obj._compute_qty_obj(
233+ cr, uid, product2uom[prod_id],
234+ stock_qty['quoted_qty'],
235+ res_uom)
236+ if ('immediately_usable_qty' in field_names):
237+ stock_qty['immediately_usable_qty'] = \
238+ uom_obj._compute_qty_obj(
239+ cr, uid, product2uom[prod_id],
240+ stock_qty['immediately_usable_qty'],
241+ res_uom)
242+ return self._update_virtual_available(cr, uid, res, context=context)
243+
244+ def _get_shops(self, cr, uid, ids, context=None):
245+ """Find the shops matching the current context
246+
247+ See the helptext for the field quoted_qty for details"""
248+ shop_ids = []
249+ # Account for one or several locations in the context
250+ # Take any shop using any warehouse that has these locations as stock
251+ # location
252+ if context.get('location', False):
253+ # Either a single or multiple locations can be in the context
254+ if not isinstance(context['location'], list):
255+ location_ids = [context['location']]
256+ else:
257+ location_ids = context['location']
258+ # Add the children locations
259+ if context.get('compute_child', True):
260+ child_location_ids = self.pool['stock.location'].search(
261+ cr, uid, [('location_id', 'child_of', location_ids)])
262+ location_ids = child_location_ids or location_ids
263+ # Get the corresponding Shops
264+ cr.execute(
265+ """
266+ SELECT id FROM sale_shop
267+ WHERE warehouse_id IN (
268+ SELECT id
269+ FROM stock_warehouse
270+ WHERE lot_stock_id IN %s)""",
271+ (tuple(location_ids),))
272+ res_location = cr.fetchone()
273+ if res_location:
274+ shop_ids.append(res_location)
275+
276+ # Account for a warehouse in the context
277+ # Take any draft order in any shop using this warehouse
278+ if context.get('warehouse', False):
279+ cr.execute("SELECT id "
280+ "FROM sale_shop "
281+ "WHERE warehouse_id = %s",
282+ (int(context['warehouse']),))
283+ res_wh = cr.fetchone()
284+ if res_wh:
285+ shop_ids.append(res_wh)
286+
287+ # If we are in a single Shop context, only count the quotations from
288+ # this shop
289+ if context.get('shop', False):
290+ shop_ids.append(context['shop'])
291+ # Build the SQL to restrict to the selected shops
292+ shop_str = ''
293+ if shop_ids:
294+ shop_str = 'AND sale_order.shop_id IN %s'
295+
296+ if shop_ids:
297+ shop_ids = (tuple(shop_ids),)
298+ else:
299+ shop_ids = ()
300+ return shop_str, shop_ids
301+
302+ def _get_dates(self, cr, uid, ids, context=None):
303+ """Build SQL criteria to match the context's from/to dates"""
304+ # If we are in a context with dates, only consider the quotations to be
305+ # delivered at these dates.
306+ # If no delivery date was entered, use the order date instead
307+ if not context:
308+ return '', ()
309+
310+ from_date = context.get('from_date', False)
311+ to_date = context.get('to_date', False)
312+ date_str = ''
313+ date_args = []
314+ if from_date:
315+ date_str = """AND COALESCE(
316+ sale_order.requested_date,
317+ sale_order.date_order) >= %s """
318+ date_args.append(from_date)
319+ if to_date:
320+ date_str += """AND COALESCE(
321+ sale_order.requested_date,
322+ sale_order.date_order) <= %s """
323+ date_args.append(to_date)
324+
325+ if date_args:
326+ date_args = (tuple(date_args),)
327+ else:
328+ date_args = ()
329+ return date_str, date_args
330+
331+ _columns = {
332+ 'quoted_qty': fields.function(
333+ _product_available_fnct, method=True, multi='qty_available',
334+ type='float',
335+ digits_compute=dp.get_precision('Product Unit of Measure'),
336+ string='Quoted',
337+ help="Total quantity of this Product that have been included in "
338+ "Quotations (Draft Sale Orders).\n"
339+ "In a context with a single Shop, this includes the "
340+ "Quotation processed at this Shop.\n"
341+ "In a context with a single Warehouse, this includes "
342+ "Quotation processed in any Shop using this Warehouse.\n"
343+ "In a context with a single Stock Location, this includes "
344+ "Quotation processed at any shop using any Warehouse using "
345+ "this Location, or any of its children, as it's Stock "
346+ "Location.\n"
347+ "Otherwise, this includes every Quotation."),
348+ }
349
350=== added file 'stock_available_sale/product_view.xml'
351--- stock_available_sale/product_view.xml 1970-01-01 00:00:00 +0000
352+++ stock_available_sale/product_view.xml 2014-05-23 07:53:49 +0000
353@@ -0,0 +1,19 @@
354+<?xml version="1.0" encoding="UTF-8"?>
355+<openerp>
356+ <data>
357+ <!-- Add the quantity available to promise in the product form -->
358+ <record id="view_product_form_quoted_qty" model="ir.ui.view">
359+ <field name="name">product.form.quoted_qty</field>
360+ <field name="model">product.product</field>
361+ <field name="type">form</field>
362+ <field name="inherit_id" ref="stock_available.view_stock_available_form" />
363+ <field name="arch" type="xml">
364+ <data>
365+ <xpath expr="//field[@name='immediately_usable_qty']" position="after">
366+ <field name="quoted_qty"/>
367+ </xpath>
368+ </data>
369+ </field>
370+ </record>
371+ </data>
372+</openerp>
373
374=== added file 'stock_available_sale/sale_stock.py'
375--- stock_available_sale/sale_stock.py 1970-01-01 00:00:00 +0000
376+++ stock_available_sale/sale_stock.py 2014-05-23 07:53:49 +0000
377@@ -0,0 +1,43 @@
378+# -*- coding: utf-8 -*-
379+##############################################################################
380+#
381+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
382+#
383+# This program is free software: you can redistribute it and/or modify
384+# it under the terms of the GNU Affero General Public License as
385+# published by the Free Software Foundation, either version 3 of the
386+# License, or (at your option) any later version.
387+#
388+# This program is distributed in the hope that it will be useful,
389+# but WITHOUT ANY WARRANTY; without even the implied warranty of
390+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
391+# GNU Affero General Public License for more details.
392+#
393+# You should have received a copy of the GNU Affero General Public License
394+# along with this program. If not, see <http://www.gnu.org/licenses/>.
395+#
396+##############################################################################
397+
398+from openerp.osv import osv
399+
400+
401+class sale_order_line(osv.osv):
402+ _inherit = "sale.order.line"
403+
404+ def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
405+ uom=False, qty_uos=0, uos=False, name='',
406+ partner_id=False, lang=False, update_tax=True,
407+ date_order=False, packaging=False,
408+ fiscal_position=False, flag=False, context=None):
409+ """Base the stock checking on the quantity available to promise
410+
411+ This is done by tweaking the context, to keep the impact minimum"""
412+ if context is None:
413+ context = {}
414+ context = dict(context, virtual_is_immediately_usable=True)
415+ return super(sale_order_line, self).product_id_change(
416+ cr, uid, ids, pricelist, product, qty=qty,
417+ uom=uom, qty_uos=qty_uos, uos=uos, name=name,
418+ partner_id=partner_id, lang=lang, update_tax=update_tax,
419+ date_order=date_order, packaging=packaging,
420+ fiscal_position=fiscal_position, flag=flag, context=context)
421
422=== added directory 'stock_available_sale/test'
423=== added file 'stock_available_sale/test/quoted_qty.yml'
424--- stock_available_sale/test/quoted_qty.yml 1970-01-01 00:00:00 +0000
425+++ stock_available_sale/test/quoted_qty.yml 2014-05-23 07:53:49 +0000
426@@ -0,0 +1,66 @@
427+- Test the computation of the quoted quantity on product.product_product_10
428+
429+- Create a UoM in the category of PCE
430+- !record {model: product.uom, id: thousand}:
431+ name: Thousand
432+ factor: 0.001
433+ rounding: 0.00
434+ uom_type: bigger
435+ category_id: product.product_uom_categ_unit
436+
437+- Cancel all the previous Quotations
438+- !python {model: sale.order}: |
439+ line_ids = self.pool['sale.order.line'].search(
440+ cr, uid, [('product_id', '=', ref('product.product_product_10')),
441+ ('state', '=', 'draft')])
442+ ids = [l.order_id.id for l in self.pool['sale.order.line'].browse(cr, uid, line_ids)]
443+ if ids:
444+ self.action_cancel(cr, uid, ids)
445+- The quoted quantity should be 0
446+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
447+ - quoted_qty == 0.0
448+
449+- Enter a Quotation
450+- !record {model: sale.order, id: order1}:
451+ order_line:
452+ - name: Quotation 1
453+ product_uom: product.product_uom_unit
454+ product_uom_qty: 107.0
455+ state: draft
456+ product_id: product.product_product_10
457+ partner_id: base.res_partner_2
458+ partner_invoice_id: base.res_partner_address_8
459+ partner_shipping_id: base.res_partner_address_8
460+ pricelist_id: product.list0
461+- The quoted qty should match the single quotation
462+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
463+ - quoted_qty == -107.0
464+
465+- Enter another Quotation
466+- !record {model: sale.order, id: order2}:
467+ order_line:
468+ - name: Quotation 1
469+ product_uom: thousand
470+ product_uom_qty: 0.613
471+ state: draft
472+ product_id: product.product_product_10
473+ partner_id: base.res_partner_2
474+ partner_invoice_id: base.res_partner_address_9
475+ partner_shipping_id: base.res_partner_address_9
476+ pricelist_id: product.list0
477+- The quoted qty should match the total of the quotations
478+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
479+ - quoted_qty == -720.0
480+- Use the context to report in another UoM
481+- !assert {model: product.product, id: product.product_product_10, string: "Check in other UoM", context: "{'uom': ref('thousand')}"}:
482+ - quoted_qty == -0.72
483+- Use the context to report in the default UoM
484+- !assert {model: product.product, id: product.product_product_10, string: "Check in False UoM", context: "{'uom': False}"}:
485+ - quoted_qty == -720.0
486+
487+- Confirm one of the Quotations
488+- !workflow {model: sale.order, action: order_confirm, ref: order1}
489+- The quoted qty should match the remaining quotation
490+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
491+ - quoted_qty == -613.0
492+

Subscribers

People subscribed via source and target branches