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
=== modified file 'stock_available/res_config.py'
--- stock_available/res_config.py 2014-05-23 07:53:49 +0000
+++ stock_available/res_config.py 2014-05-23 07:53:49 +0000
@@ -31,4 +31,11 @@
31 help="This will subtract incoming quantities from the quantities"31 help="This will subtract incoming quantities from the quantities"
32 "available to promise.\n"32 "available to promise.\n"
33 "This installs the module stock_available_immediately."),33 "This installs the module stock_available_immediately."),
34 'module_stock_available_sale': fields.boolean(
35 'Exclude goods already in sale quotations',
36 help="This will subtract quantities from the sale quotations from"
37 "the quantities available to promise.\n"
38 "This installs the modules stock_available_sale.\n"
39 "If the modules sale and sale_delivery_date are not "
40 "installed, this will install them too"),
34 }41 }
3542
=== modified file 'stock_available/res_config_view.xml'
--- stock_available/res_config_view.xml 2014-05-23 07:53:49 +0000
+++ stock_available/res_config_view.xml 2014-05-23 07:53:49 +0000
@@ -15,6 +15,10 @@
15 <field name="module_stock_available_immediately" class="oe_inline" />15 <field name="module_stock_available_immediately" class="oe_inline" />
16 <label for="module_stock_available_immediately" />16 <label for="module_stock_available_immediately" />
17 </div>17 </div>
18 <div>
19 <field name="module_stock_available_sale" class="oe_inline" />
20 <label for="module_stock_available_sale" />
21 </div>
18 </div>22 </div>
19 </group>23 </group>
20 </xpath>24 </xpath>
2125
=== added directory 'stock_available_sale'
=== added file 'stock_available_sale/__init__.py'
--- stock_available_sale/__init__.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/__init__.py 2014-05-23 07:53:49 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21from . import product
22from . import sale_stock
023
=== added file 'stock_available_sale/__openerp__.py'
--- stock_available_sale/__openerp__.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/__openerp__.py 2014-05-23 07:53:49 +0000
@@ -0,0 +1,48 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21{
22 'name': 'Quotations in quantity available to promise',
23 'version': '2.0',
24 'author': u'Numérigraphe SÀRL',
25 'category': 'Hidden',
26 'depends': [
27 'stock_available',
28 'sale_order_dates',
29 'sale_stock',
30 ],
31 'description': """
32This module computes the quoted quantity of the Products, and subtracts it from
33the quantities available to promise .
34
35"Quoted" is defined as the sum of the quantities of this product in Quotations,
36taking the context's shop or warehouse into account.
37
38When entering sale orders, the salesperson get warned if the quantity available
39to promise is insufficient (instead when the virtual stock is
40insufficient).""",
41 'data': [
42 'product_view.xml',
43 ],
44 'test': [
45 'test/quoted_qty.yml',
46 ],
47 'license': 'AGPL-3',
48}
049
=== added file 'stock_available_sale/product.py'
--- stock_available_sale/product.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/product.py 2014-05-23 07:53:49 +0000
@@ -0,0 +1,232 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21from openerp.osv import orm, fields
22import decimal_precision as dp
23
24# Function which uses the pool to call the method from the other modules too.
25from openerp.addons.stock_available import _product_available_fnct
26
27
28class ProductProduct(orm.Model):
29 """Add the computation for the stock available to promise"""
30 _inherit = 'product.product'
31
32 def _product_available(self, cr, uid, ids, field_names=None, arg=False,
33 context=None):
34 """Compute the quantities in Quotations."""
35 # Check the context
36 if context is None:
37 context = {}
38 # Prepare an alternative context without 'uom', to avoid cross-category
39 # conversions when reading the available stock of components
40 if 'uom' in context:
41 context_wo_uom = context.copy()
42 del context_wo_uom['uom']
43 else:
44 context_wo_uom = context
45
46 if field_names is None:
47 field_names = []
48
49 # Compute the core quantities
50 res = super(ProductProduct, self)._product_available(
51 cr, uid, ids, field_names=field_names, arg=arg, context=context)
52
53 # Compute the quantities quoted/available to promise
54 if ('quoted_qty' in field_names
55 or 'immediately_usable_qty' in field_names):
56 date_str, date_args = self._get_dates(cr, uid, ids,
57 context=context)
58
59 # Limit the search to some shops according to the context
60 shop_str, shop_args = self._get_shops(cr, uid, ids,
61 context=context)
62
63 # Query the total by Product and UoM
64 cr.execute(
65 """
66 SELECT sum(product_uom_qty), product_id, product_uom
67 FROM sale_order_line
68 INNER JOIN sale_order
69 ON (sale_order_line.order_id = sale_order.id)
70 WHERE product_id in %s
71 AND sale_order_line.state = 'draft' """
72 + date_str + shop_str +
73 "GROUP BY sale_order_line.product_id, product_uom",
74 (tuple(ids),) + date_args + shop_args)
75 results = cr.fetchall()
76
77 # Get the UoM resources we'll need for conversion
78 # UoMs from the products
79 uoms_o = {}
80 product2uom = {}
81 for product in self.browse(cr, uid, ids, context=context):
82 product2uom[product.id] = product.uom_id
83 uoms_o[product.uom_id.id] = product.uom_id
84 # UoM from the results and the context
85 uom_obj = self.pool['product.uom']
86 uoms = map(lambda stock_product_uom_qty: stock_product_uom_qty[2],
87 results)
88 if context.get('uom', False):
89 uoms.append(context['uom'])
90 uoms = filter(lambda stock_product_uom_qty:
91 stock_product_uom_qty not in uoms_o.keys(), uoms)
92 if uoms:
93 uoms = uom_obj.browse(cr, uid, list(set(uoms)),
94 context=context)
95 for o in uoms:
96 uoms_o[o.id] = o
97
98 # Compute the quoted quantity
99 for (amount, prod_id, prod_uom) in results:
100 # Convert the amount to the product's UoM without rounding
101 amount = amount / uoms_o[prod_uom].factor
102 if ('quoted_qty' in field_names):
103 res[prod_id]['quoted_qty'] -= amount
104 if ('immediately_usable_qty' in field_names):
105 res[prod_id]['immediately_usable_qty'] -= amount
106
107 # Round and optionally convert the results to the requested UoM
108 for prod_id, stock_qty in res.iteritems():
109 if context.get('uom', False):
110 # Convert to the requested UoM
111 res_uom = uoms_o[context['uom']]
112 else:
113 # The conversion is unneeded but we do need the rounding
114 res_uom = product2uom[prod_id]
115 if ('quoted_qty' in field_names):
116 stock_qty['quoted_qty'] = uom_obj._compute_qty_obj(
117 cr, uid, product2uom[prod_id],
118 stock_qty['quoted_qty'],
119 res_uom)
120 if ('immediately_usable_qty' in field_names):
121 stock_qty['immediately_usable_qty'] = \
122 uom_obj._compute_qty_obj(
123 cr, uid, product2uom[prod_id],
124 stock_qty['immediately_usable_qty'],
125 res_uom)
126 return self._update_virtual_available(cr, uid, res, context=context)
127
128 def _get_shops(self, cr, uid, ids, context=None):
129 """Find the shops matching the current context
130
131 See the helptext for the field quoted_qty for details"""
132 shop_ids = []
133 # Account for one or several locations in the context
134 # Take any shop using any warehouse that has these locations as stock
135 # location
136 if context.get('location', False):
137 # Either a single or multiple locations can be in the context
138 if not isinstance(context['location'], list):
139 location_ids = [context['location']]
140 else:
141 location_ids = context['location']
142 # Add the children locations
143 if context.get('compute_child', True):
144 child_location_ids = self.pool['stock.location'].search(
145 cr, uid, [('location_id', 'child_of', location_ids)])
146 location_ids = child_location_ids or location_ids
147 # Get the corresponding Shops
148 cr.execute(
149 """
150 SELECT id FROM sale_shop
151 WHERE warehouse_id IN (
152 SELECT id
153 FROM stock_warehouse
154 WHERE lot_stock_id IN %s)""",
155 (tuple(location_ids),))
156 res_location = cr.fetchone()
157 if res_location:
158 shop_ids.append(res_location)
159
160 # Account for a warehouse in the context
161 # Take any draft order in any shop using this warehouse
162 if context.get('warehouse', False):
163 cr.execute("SELECT id "
164 "FROM sale_shop "
165 "WHERE warehouse_id = %s",
166 (int(context['warehouse']),))
167 res_wh = cr.fetchone()
168 if res_wh:
169 shop_ids.append(res_wh)
170
171 # If we are in a single Shop context, only count the quotations from
172 # this shop
173 if context.get('shop', False):
174 shop_ids.append(context['shop'])
175 # Build the SQL to restrict to the selected shops
176 shop_str = ''
177 if shop_ids:
178 shop_str = 'AND sale_order.shop_id IN %s'
179
180 if shop_ids:
181 shop_ids = (tuple(shop_ids),)
182 else:
183 shop_ids = ()
184 return shop_str, shop_ids
185
186 def _get_dates(self, cr, uid, ids, context=None):
187 """Build SQL criteria to match the context's from/to dates"""
188 # If we are in a context with dates, only consider the quotations to be
189 # delivered at these dates.
190 # If no delivery date was entered, use the order date instead
191 if not context:
192 return '', ()
193
194 from_date = context.get('from_date', False)
195 to_date = context.get('to_date', False)
196 date_str = ''
197 date_args = []
198 if from_date:
199 date_str = """AND COALESCE(
200 sale_order.requested_date,
201 sale_order.date_order) >= %s """
202 date_args.append(from_date)
203 if to_date:
204 date_str += """AND COALESCE(
205 sale_order.requested_date,
206 sale_order.date_order) <= %s """
207 date_args.append(to_date)
208
209 if date_args:
210 date_args = (tuple(date_args),)
211 else:
212 date_args = ()
213 return date_str, date_args
214
215 _columns = {
216 'quoted_qty': fields.function(
217 _product_available_fnct, method=True, multi='qty_available',
218 type='float',
219 digits_compute=dp.get_precision('Product Unit of Measure'),
220 string='Quoted',
221 help="Total quantity of this Product that have been included in "
222 "Quotations (Draft Sale Orders).\n"
223 "In a context with a single Shop, this includes the "
224 "Quotation processed at this Shop.\n"
225 "In a context with a single Warehouse, this includes "
226 "Quotation processed in any Shop using this Warehouse.\n"
227 "In a context with a single Stock Location, this includes "
228 "Quotation processed at any shop using any Warehouse using "
229 "this Location, or any of its children, as it's Stock "
230 "Location.\n"
231 "Otherwise, this includes every Quotation."),
232 }
0233
=== added file 'stock_available_sale/product_view.xml'
--- stock_available_sale/product_view.xml 1970-01-01 00:00:00 +0000
+++ stock_available_sale/product_view.xml 2014-05-23 07:53:49 +0000
@@ -0,0 +1,19 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<openerp>
3 <data>
4 <!-- Add the quantity available to promise in the product form -->
5 <record id="view_product_form_quoted_qty" model="ir.ui.view">
6 <field name="name">product.form.quoted_qty</field>
7 <field name="model">product.product</field>
8 <field name="type">form</field>
9 <field name="inherit_id" ref="stock_available.view_stock_available_form" />
10 <field name="arch" type="xml">
11 <data>
12 <xpath expr="//field[@name='immediately_usable_qty']" position="after">
13 <field name="quoted_qty"/>
14 </xpath>
15 </data>
16 </field>
17 </record>
18 </data>
19</openerp>
020
=== added file 'stock_available_sale/sale_stock.py'
--- stock_available_sale/sale_stock.py 1970-01-01 00:00:00 +0000
+++ stock_available_sale/sale_stock.py 2014-05-23 07:53:49 +0000
@@ -0,0 +1,43 @@
1# -*- coding: utf-8 -*-
2##############################################################################
3#
4# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU Affero General Public License as
8# published by the Free Software Foundation, either version 3 of the
9# License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19##############################################################################
20
21from openerp.osv import osv
22
23
24class sale_order_line(osv.osv):
25 _inherit = "sale.order.line"
26
27 def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
28 uom=False, qty_uos=0, uos=False, name='',
29 partner_id=False, lang=False, update_tax=True,
30 date_order=False, packaging=False,
31 fiscal_position=False, flag=False, context=None):
32 """Base the stock checking on the quantity available to promise
33
34 This is done by tweaking the context, to keep the impact minimum"""
35 if context is None:
36 context = {}
37 context = dict(context, virtual_is_immediately_usable=True)
38 return super(sale_order_line, self).product_id_change(
39 cr, uid, ids, pricelist, product, qty=qty,
40 uom=uom, qty_uos=qty_uos, uos=uos, name=name,
41 partner_id=partner_id, lang=lang, update_tax=update_tax,
42 date_order=date_order, packaging=packaging,
43 fiscal_position=fiscal_position, flag=flag, context=context)
044
=== added directory 'stock_available_sale/test'
=== added file 'stock_available_sale/test/quoted_qty.yml'
--- stock_available_sale/test/quoted_qty.yml 1970-01-01 00:00:00 +0000
+++ stock_available_sale/test/quoted_qty.yml 2014-05-23 07:53:49 +0000
@@ -0,0 +1,66 @@
1- Test the computation of the quoted quantity on product.product_product_10
2
3- Create a UoM in the category of PCE
4- !record {model: product.uom, id: thousand}:
5 name: Thousand
6 factor: 0.001
7 rounding: 0.00
8 uom_type: bigger
9 category_id: product.product_uom_categ_unit
10
11- Cancel all the previous Quotations
12- !python {model: sale.order}: |
13 line_ids = self.pool['sale.order.line'].search(
14 cr, uid, [('product_id', '=', ref('product.product_product_10')),
15 ('state', '=', 'draft')])
16 ids = [l.order_id.id for l in self.pool['sale.order.line'].browse(cr, uid, line_ids)]
17 if ids:
18 self.action_cancel(cr, uid, ids)
19- The quoted quantity should be 0
20- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
21 - quoted_qty == 0.0
22
23- Enter a Quotation
24- !record {model: sale.order, id: order1}:
25 order_line:
26 - name: Quotation 1
27 product_uom: product.product_uom_unit
28 product_uom_qty: 107.0
29 state: draft
30 product_id: product.product_product_10
31 partner_id: base.res_partner_2
32 partner_invoice_id: base.res_partner_address_8
33 partner_shipping_id: base.res_partner_address_8
34 pricelist_id: product.list0
35- The quoted qty should match the single quotation
36- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
37 - quoted_qty == -107.0
38
39- Enter another Quotation
40- !record {model: sale.order, id: order2}:
41 order_line:
42 - name: Quotation 1
43 product_uom: thousand
44 product_uom_qty: 0.613
45 state: draft
46 product_id: product.product_product_10
47 partner_id: base.res_partner_2
48 partner_invoice_id: base.res_partner_address_9
49 partner_shipping_id: base.res_partner_address_9
50 pricelist_id: product.list0
51- The quoted qty should match the total of the quotations
52- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
53 - quoted_qty == -720.0
54- Use the context to report in another UoM
55- !assert {model: product.product, id: product.product_product_10, string: "Check in other UoM", context: "{'uom': ref('thousand')}"}:
56 - quoted_qty == -0.72
57- Use the context to report in the default UoM
58- !assert {model: product.product, id: product.product_product_10, string: "Check in False UoM", context: "{'uom': False}"}:
59 - quoted_qty == -720.0
60
61- Confirm one of the Quotations
62- !workflow {model: sale.order, action: order_confirm, ref: order1}
63- The quoted qty should match the remaining quotation
64- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
65 - quoted_qty == -613.0
66

Subscribers

People subscribed via source and target branches