[ADD] stock_available_sale

Take sale quotations into account in the stock quantity available to promise
Cherry-picked from 497068f5f5

Conflicts:
	stock_available/res_config.py
This commit is contained in:
Lionel Sausin (Numérigraphe)
2014-11-28 19:49:35 +01:00
committed by Lionel Sausin
parent 6cc05a8215
commit bd3c969bfc
9 changed files with 488 additions and 0 deletions

View File

@@ -31,6 +31,14 @@ class StockConfig(models.TransientModel):
"available to promise.\n"
"This installs the module stock_available_immediately.")
module_stock_available_sale = fields.Boolean(
string='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")
# module_stock_available_mrp = fields.Boolean(
# string='Include the production potential',
# help="This will add the quantities of goods that can be "

View File

@@ -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>
<field name="module_stock_available_mrp" class="oe_inline" />
<label for="module_stock_available_mrp" />

View File

@@ -0,0 +1,21 @@
# -*- 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

View File

@@ -0,0 +1,44 @@
# -*- 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.""",
'data': [
'product_view.xml',
],
'test': [
'test/quoted_qty.yml',
],
'license': 'AGPL-3',
}

View File

@@ -0,0 +1,49 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * stock_available_sale
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-30 19:35+0000\n"
"PO-Revision-Date: 2014-07-30 19:35+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: stock_available_sale
#: code:_description:0
#: model:ir.model,name:stock_available_sale.model_product_product
#, python-format
msgid "Product"
msgstr "Article"
#. module: stock_available_sale
#: field:product.product,quoted_qty:0
msgid "Quoted"
msgstr "En devis"
#. module: stock_available_sale
#: code:_description:0
#: model:ir.model,name:stock_available_sale.model_sale_order_line
#, python-format
msgid "Sales Order Line"
msgstr "Ligne de commandes de vente"
#. module: stock_available_sale
#: help:product.product,quoted_qty:0
msgid "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."
msgstr "Quantité totale de cet article se trouvant déjà dans les devis (commandes de vente brouillon).\n"
"Quand le contexte indique une boutique, cela comprend les devis traités dans cette boutique.\n"
"Quand le contexte indique un entrepôt, cela comprend les devis traités dans toutes les boutiques utilisant cet entrepôt.\n"
"Quand le contexte indique un emplacement de stock, cela comprend les devis traités dans toutes les boutiques utilisant un des entrepôt utilisant cet emplacement, ou un de ses enfants, comme emplacement de stock.\n"
"Dans les autres cas, cela comprend tous les devis."

View File

@@ -0,0 +1,45 @@
# Translation of OpenERP Server.
# This file contains the translation of the following modules:
# * stock_available_sale
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-30 19:42+0000\n"
"PO-Revision-Date: 2014-07-30 19:42+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: stock_available_sale
#: code:_description:0
#: model:ir.model,name:stock_available_sale.model_product_product
#, python-format
msgid "Product"
msgstr ""
#. module: stock_available_sale
#: field:product.product,quoted_qty:0
msgid "Quoted"
msgstr ""
#. module: stock_available_sale
#: code:_description:0
#: model:ir.model,name:stock_available_sale.model_sale_order_line
#, python-format
msgid "Sales Order Line"
msgstr ""
#. module: stock_available_sale
#: help:product.product,quoted_qty:0
msgid "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."
msgstr ""

View File

@@ -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 import SUPERUSER_ID
from openerp.osv import orm, fields
import openerp.addons.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."""
# Compute the core quantities
res = super(ProductProduct, self)._product_available(
cr, uid, ids, field_names=field_names, arg=arg, context=context)
# If we didn't get a field_names list, there's nothing to do
if field_names is None:
return res
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
# Compute the quantities quoted/available to promise
if any([f in field_names
for f in ['quoted_qty', 'immediately_usable_qty']]):
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, SUPERUSER_ID, 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, SUPERUSER_ID, 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, SUPERUSER_ID, product2uom[prod_id],
stock_qty['immediately_usable_qty'],
res_uom)
return res
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, SUPERUSER_ID,
[('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."),
}

View File

@@ -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>

View File

@@ -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