openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #06932
[Merge] lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale into lp:stock-logistic-warehouse
Lionel Sausin - Numérigraphe has proposed merging lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale into lp:stock-logistic-warehouse with lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available as a prerequisite.
Requested reviews:
Stock and Logistic Core Editors (stock-logistic-core-editors)
For more details, see:
https://code.launchpad.net/~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale/+merge/220761
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.
Module Co-authored by Loïc Bellier and your humble servant.
--
https://code.launchpad.net/~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale/+merge/220761
Your team Stock and Logistic Core Editors is requested to review the proposed merge of lp:~numerigraphe-team/stock-logistic-warehouse/7.0-add-stock-available-sale into lp:stock-logistic-warehouse.
=== 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 @@
help="This will subtract incoming quantities from the quantities"
"available to promise.\n"
"This installs the module stock_available_immediately."),
+ 'module_stock_available_sale': fields.boolean(
+ 'Exclude goods already in sale quotations',
+ help="This will subtract quantities from the sale quotations from"
+ "the quantities available to promise.\n"
+ "This installs the modules stock_available_sale.\n"
+ "If the modules sale and sale_delivery_date are not "
+ "installed, this will install them too"),
}
=== 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 @@
<field name="module_stock_available_immediately" class="oe_inline" />
<label for="module_stock_available_immediately" />
</div>
+ <div>
+ <field name="module_stock_available_sale" class="oe_inline" />
+ <label for="module_stock_available_sale" />
+ </div>
</div>
</group>
</xpath>
=== 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 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from . import product
+from . import sale_stock
=== 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 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+{
+ 'name': 'Quotations in quantity available to promise',
+ 'version': '2.0',
+ 'author': u'Numérigraphe SÀRL',
+ 'category': 'Hidden',
+ 'depends': [
+ 'stock_available',
+ 'sale_order_dates',
+ 'sale_stock',
+ ],
+ 'description': """
+This module computes the quoted quantity of the Products, and subtracts it from
+the quantities available to promise .
+
+"Quoted" is defined as the sum of the quantities of this product in Quotations,
+taking the context's shop or warehouse into account.
+
+When entering sale orders, the salesperson get warned if the quantity available
+to promise is insufficient (instead when the virtual stock is
+insufficient).""",
+ 'data': [
+ 'product_view.xml',
+ ],
+ 'test': [
+ 'test/quoted_qty.yml',
+ ],
+ 'license': 'AGPL-3',
+}
=== 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 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv import orm, fields
+import decimal_precision as dp
+
+# Function which uses the pool to call the method from the other modules too.
+from openerp.addons.stock_available import _product_available_fnct
+
+
+class ProductProduct(orm.Model):
+ """Add the computation for the stock available to promise"""
+ _inherit = 'product.product'
+
+ def _product_available(self, cr, uid, ids, field_names=None, arg=False,
+ context=None):
+ """Compute the quantities in Quotations."""
+ # Check the context
+ if context is None:
+ context = {}
+ # Prepare an alternative context without 'uom', to avoid cross-category
+ # conversions when reading the available stock of components
+ if 'uom' in context:
+ context_wo_uom = context.copy()
+ del context_wo_uom['uom']
+ else:
+ context_wo_uom = context
+
+ if field_names is None:
+ field_names = []
+
+ # Compute the core quantities
+ res = super(ProductProduct, self)._product_available(
+ cr, uid, ids, field_names=field_names, arg=arg, context=context)
+
+ # Compute the quantities quoted/available to promise
+ if ('quoted_qty' in field_names
+ or 'immediately_usable_qty' in field_names):
+ date_str, date_args = self._get_dates(cr, uid, ids,
+ context=context)
+
+ # Limit the search to some shops according to the context
+ shop_str, shop_args = self._get_shops(cr, uid, ids,
+ context=context)
+
+ # Query the total by Product and UoM
+ cr.execute(
+ """
+ SELECT sum(product_uom_qty), product_id, product_uom
+ FROM sale_order_line
+ INNER JOIN sale_order
+ ON (sale_order_line.order_id = sale_order.id)
+ WHERE product_id in %s
+ AND sale_order_line.state = 'draft' """
+ + date_str + shop_str +
+ "GROUP BY sale_order_line.product_id, product_uom",
+ (tuple(ids),) + date_args + shop_args)
+ results = cr.fetchall()
+
+ # Get the UoM resources we'll need for conversion
+ # UoMs from the products
+ uoms_o = {}
+ product2uom = {}
+ for product in self.browse(cr, uid, ids, context=context):
+ product2uom[product.id] = product.uom_id
+ uoms_o[product.uom_id.id] = product.uom_id
+ # UoM from the results and the context
+ uom_obj = self.pool['product.uom']
+ uoms = map(lambda stock_product_uom_qty: stock_product_uom_qty[2],
+ results)
+ if context.get('uom', False):
+ uoms.append(context['uom'])
+ uoms = filter(lambda stock_product_uom_qty:
+ stock_product_uom_qty not in uoms_o.keys(), uoms)
+ if uoms:
+ uoms = uom_obj.browse(cr, uid, list(set(uoms)),
+ context=context)
+ for o in uoms:
+ uoms_o[o.id] = o
+
+ # Compute the quoted quantity
+ for (amount, prod_id, prod_uom) in results:
+ # Convert the amount to the product's UoM without rounding
+ amount = amount / uoms_o[prod_uom].factor
+ if ('quoted_qty' in field_names):
+ res[prod_id]['quoted_qty'] -= amount
+ if ('immediately_usable_qty' in field_names):
+ res[prod_id]['immediately_usable_qty'] -= amount
+
+ # Round and optionally convert the results to the requested UoM
+ for prod_id, stock_qty in res.iteritems():
+ if context.get('uom', False):
+ # Convert to the requested UoM
+ res_uom = uoms_o[context['uom']]
+ else:
+ # The conversion is unneeded but we do need the rounding
+ res_uom = product2uom[prod_id]
+ if ('quoted_qty' in field_names):
+ stock_qty['quoted_qty'] = uom_obj._compute_qty_obj(
+ cr, uid, product2uom[prod_id],
+ stock_qty['quoted_qty'],
+ res_uom)
+ if ('immediately_usable_qty' in field_names):
+ stock_qty['immediately_usable_qty'] = \
+ uom_obj._compute_qty_obj(
+ cr, uid, product2uom[prod_id],
+ stock_qty['immediately_usable_qty'],
+ res_uom)
+ return self._update_virtual_available(cr, uid, res, context=context)
+
+ def _get_shops(self, cr, uid, ids, context=None):
+ """Find the shops matching the current context
+
+ See the helptext for the field quoted_qty for details"""
+ shop_ids = []
+ # Account for one or several locations in the context
+ # Take any shop using any warehouse that has these locations as stock
+ # location
+ if context.get('location', False):
+ # Either a single or multiple locations can be in the context
+ if not isinstance(context['location'], list):
+ location_ids = [context['location']]
+ else:
+ location_ids = context['location']
+ # Add the children locations
+ if context.get('compute_child', True):
+ child_location_ids = self.pool['stock.location'].search(
+ cr, uid, [('location_id', 'child_of', location_ids)])
+ location_ids = child_location_ids or location_ids
+ # Get the corresponding Shops
+ cr.execute(
+ """
+ SELECT id FROM sale_shop
+ WHERE warehouse_id IN (
+ SELECT id
+ FROM stock_warehouse
+ WHERE lot_stock_id IN %s)""",
+ (tuple(location_ids),))
+ res_location = cr.fetchone()
+ if res_location:
+ shop_ids.append(res_location)
+
+ # Account for a warehouse in the context
+ # Take any draft order in any shop using this warehouse
+ if context.get('warehouse', False):
+ cr.execute("SELECT id "
+ "FROM sale_shop "
+ "WHERE warehouse_id = %s",
+ (int(context['warehouse']),))
+ res_wh = cr.fetchone()
+ if res_wh:
+ shop_ids.append(res_wh)
+
+ # If we are in a single Shop context, only count the quotations from
+ # this shop
+ if context.get('shop', False):
+ shop_ids.append(context['shop'])
+ # Build the SQL to restrict to the selected shops
+ shop_str = ''
+ if shop_ids:
+ shop_str = 'AND sale_order.shop_id IN %s'
+
+ if shop_ids:
+ shop_ids = (tuple(shop_ids),)
+ else:
+ shop_ids = ()
+ return shop_str, shop_ids
+
+ def _get_dates(self, cr, uid, ids, context=None):
+ """Build SQL criteria to match the context's from/to dates"""
+ # If we are in a context with dates, only consider the quotations to be
+ # delivered at these dates.
+ # If no delivery date was entered, use the order date instead
+ if not context:
+ return '', ()
+
+ from_date = context.get('from_date', False)
+ to_date = context.get('to_date', False)
+ date_str = ''
+ date_args = []
+ if from_date:
+ date_str = """AND COALESCE(
+ sale_order.requested_date,
+ sale_order.date_order) >= %s """
+ date_args.append(from_date)
+ if to_date:
+ date_str += """AND COALESCE(
+ sale_order.requested_date,
+ sale_order.date_order) <= %s """
+ date_args.append(to_date)
+
+ if date_args:
+ date_args = (tuple(date_args),)
+ else:
+ date_args = ()
+ return date_str, date_args
+
+ _columns = {
+ 'quoted_qty': fields.function(
+ _product_available_fnct, method=True, multi='qty_available',
+ type='float',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ string='Quoted',
+ help="Total quantity of this Product that have been included in "
+ "Quotations (Draft Sale Orders).\n"
+ "In a context with a single Shop, this includes the "
+ "Quotation processed at this Shop.\n"
+ "In a context with a single Warehouse, this includes "
+ "Quotation processed in any Shop using this Warehouse.\n"
+ "In a context with a single Stock Location, this includes "
+ "Quotation processed at any shop using any Warehouse using "
+ "this Location, or any of its children, as it's Stock "
+ "Location.\n"
+ "Otherwise, this includes every Quotation."),
+ }
=== 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<openerp>
+ <data>
+ <!-- Add the quantity available to promise in the product form -->
+ <record id="view_product_form_quoted_qty" model="ir.ui.view">
+ <field name="name">product.form.quoted_qty</field>
+ <field name="model">product.product</field>
+ <field name="type">form</field>
+ <field name="inherit_id" ref="stock_available.view_stock_available_form" />
+ <field name="arch" type="xml">
+ <data>
+ <xpath expr="//field[@name='immediately_usable_qty']" position="after">
+ <field name="quoted_qty"/>
+ </xpath>
+ </data>
+ </field>
+ </record>
+ </data>
+</openerp>
=== 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 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# This module is copyright (C) 2014 Numérigraphe SARL. All Rights Reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv import osv
+
+
+class sale_order_line(osv.osv):
+ _inherit = "sale.order.line"
+
+ def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
+ uom=False, qty_uos=0, uos=False, name='',
+ partner_id=False, lang=False, update_tax=True,
+ date_order=False, packaging=False,
+ fiscal_position=False, flag=False, context=None):
+ """Base the stock checking on the quantity available to promise
+
+ This is done by tweaking the context, to keep the impact minimum"""
+ if context is None:
+ context = {}
+ context = dict(context, virtual_is_immediately_usable=True)
+ return super(sale_order_line, self).product_id_change(
+ cr, uid, ids, pricelist, product, qty=qty,
+ uom=uom, qty_uos=qty_uos, uos=uos, name=name,
+ partner_id=partner_id, lang=lang, update_tax=update_tax,
+ date_order=date_order, packaging=packaging,
+ fiscal_position=fiscal_position, flag=flag, context=context)
=== 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 @@
+- Test the computation of the quoted quantity on product.product_product_10
+
+- Create a UoM in the category of PCE
+- !record {model: product.uom, id: thousand}:
+ name: Thousand
+ factor: 0.001
+ rounding: 0.00
+ uom_type: bigger
+ category_id: product.product_uom_categ_unit
+
+- Cancel all the previous Quotations
+- !python {model: sale.order}: |
+ line_ids = self.pool['sale.order.line'].search(
+ cr, uid, [('product_id', '=', ref('product.product_product_10')),
+ ('state', '=', 'draft')])
+ ids = [l.order_id.id for l in self.pool['sale.order.line'].browse(cr, uid, line_ids)]
+ if ids:
+ self.action_cancel(cr, uid, ids)
+- The quoted quantity should be 0
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
+ - quoted_qty == 0.0
+
+- Enter a Quotation
+- !record {model: sale.order, id: order1}:
+ order_line:
+ - name: Quotation 1
+ product_uom: product.product_uom_unit
+ product_uom_qty: 107.0
+ state: draft
+ product_id: product.product_product_10
+ partner_id: base.res_partner_2
+ partner_invoice_id: base.res_partner_address_8
+ partner_shipping_id: base.res_partner_address_8
+ pricelist_id: product.list0
+- The quoted qty should match the single quotation
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted_qty"}:
+ - quoted_qty == -107.0
+
+- Enter another Quotation
+- !record {model: sale.order, id: order2}:
+ order_line:
+ - name: Quotation 1
+ product_uom: thousand
+ product_uom_qty: 0.613
+ state: draft
+ product_id: product.product_product_10
+ partner_id: base.res_partner_2
+ partner_invoice_id: base.res_partner_address_9
+ partner_shipping_id: base.res_partner_address_9
+ pricelist_id: product.list0
+- The quoted qty should match the total of the quotations
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
+ - quoted_qty == -720.0
+- Use the context to report in another UoM
+- !assert {model: product.product, id: product.product_product_10, string: "Check in other UoM", context: "{'uom': ref('thousand')}"}:
+ - quoted_qty == -0.72
+- Use the context to report in the default UoM
+- !assert {model: product.product, id: product.product_product_10, string: "Check in False UoM", context: "{'uom': False}"}:
+ - quoted_qty == -720.0
+
+- Confirm one of the Quotations
+- !workflow {model: sale.order, action: order_confirm, ref: order1}
+- The quoted qty should match the remaining quotation
+- !assert {model: product.product, id: product.product_product_10, string: "Check quoted quantity"}:
+ - quoted_qty == -613.0
+
Follow ups