new module 'stock_available_unreserved' (#206)

* [ADD] new module 'stock_available_unreserved'
This commit is contained in:
Jordi Ballester Alomar
2017-02-27 11:29:19 +01:00
committed by sbejaoui
parent 8bb6375f92
commit 1d6f01f59e
10 changed files with 492 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
==========================
Stock Available Unreserved
==========================
This module allows users to check the quantity of a stocked product that is
available on-hand, and that has not yet been reserved for use anywhere else.
This key figure is very important during the monitoring of the warehouse
execution, because it assists users to ensure that the flow of products will
not be stuck due to a sudden unavailability of stock.
If the warehouse personnel ensures that the unreserved quantity on hand > 0,
then nobody will be stuck in pickings or manufacturing orders waiting for
the availability of unreserved stock.
Usage
=====
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/153/9.0
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/stock-logistics-warehouse/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Stock Available Unreserved",
"summary": "Quantity of stock available for inmediate use",
"version": "9.0.1.0.0",
"author": "Eficent Business and IT Consulting Services S.L,"
"Odoo Community Association (OCA)",
"website": "https://www.odoo-community.org",
"category": "Warehouse Management",
"depends": ["stock"],
"data": ["views/stock_quant_view.xml",
"views/product_view.xml"
],
"license": "AGPL-3",
'installable': True,
'application': False,
}

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import product

View File

@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models, _
from openerp.tools.float_utils import float_round
from openerp.addons import decimal_precision as dp
UNIT = dp.get_precision('Product Unit of Measure')
class ProductTemplate(models.Model):
_inherit = "product.template"
qty_available_not_res = fields.Float(
string='Quantity On Hand Unreserved', digits=UNIT,
compute='_compute_product_available_not_res')
qty_available_stock_text = fields.Char(
compute='_compute_product_available_not_res',
string='Unreserved stock quantity')
@api.multi
def _compute_product_available_not_res(self):
no_new = self.filtered(
lambda x: not isinstance(x.id, models.NewId))
res = no_new._product_available()
for tmpl in no_new:
qty = res[tmpl.id]['qty_available_not_res']
tmpl.qty_available_not_res = qty
text = res[tmpl.id]['qty_available_stock_text']
tmpl.qty_available_stock_text = text
@api.multi
def _product_available(self, name=None, arg=False):
prod_available = super(ProductTemplate, self)._product_available(name,
arg)
variants = self.env['product.product']
for product in self:
variants += product.product_variant_ids
variant_available = variants._product_available()
for product in self:
if isinstance(product.id, models.NewId):
continue
qty_available_not_res = 0.0
text = ''
for p in product.product_variant_ids:
qty = variant_available[p.id]["qty_available_not_res"]
qty_available_not_res += qty
text = variant_available[p.id]["qty_available_stock_text"]
prod_available[product.id].update({
"qty_available_not_res": qty_available_not_res,
"qty_available_stock_text": text,
})
return prod_available
@api.multi
def action_open_quants_unreserved(self):
products = self._get_products()
result = self._get_act_window_dict('stock.product_open_quants')
result['domain'] = "[('product_id','in',[" + ','.join(
map(str, products)) + "]), ('reservation_id', '=', False)]"
result[
'context'] = "{'search_default_locationgroup': 1, " \
"'search_default_internal_loc': 1}"
return result
class ProductProduct(models.Model):
_inherit = 'product.product'
qty_available_not_res = fields.Float(
string='Qty Available Not Reserved', digits=UNIT,
compute='_compute_qty_available_not_res')
qty_available_stock_text = fields.Char(
compute='_compute_qty_available_not_res', string='Available per stock')
@api.multi
def _compute_qty_available_not_res(self):
res = self._product_available()
for prod in self:
qty = res[prod.id]['qty_available_not_res']
text = res[prod.id]['qty_available_stock_text']
prod.qty_available_not_res = qty
prod.qty_available_stock_text = text
@api.model
def _prepare_domain_available_not_res(self, products):
domain_products = [('product_id', 'in', products.mapped('id'))]
domain_quant = []
domain_quant_loc, _, _ = products._get_domain_locations()
domain_quant += domain_products
domain_quant.append(('reservation_id', '=', False))
domain_quant += domain_quant_loc
return domain_quant
@api.multi
def _product_available_not_res_hook(self, quants):
"""Hook used to introduce possible variations"""
return False
@api.multi
def _product_available(self, field_names=None, arg=False):
res = super(ProductProduct, self).\
_product_available(field_names=field_names,
arg=arg)
domain_quant = self._prepare_domain_available_not_res(self)
quants = self.env['stock.quant'].read_group(
domain_quant,
['product_id', 'location_id', 'qty'],
['product_id', 'location_id'],
lazy=False)
values_prod = {}
for quant in quants:
# create a dictionary with the total value per products
values_prod.setdefault(quant['product_id'][0], 0)
values_prod[quant['product_id'][0]] += quant['qty']
for product in self:
# get total qty for the product
qty = float_round(values_prod.get(product.id, 0.0),
precision_rounding=product.uom_id.rounding)
qty_available_not_res = qty
res[product.id].update({'qty_available_not_res':
qty_available_not_res})
text = str(qty_available_not_res) + _(" On Hand")
res[product.id].update({'qty_available_stock_text': text})
self._product_available_not_res_hook(quants)
return res

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import test_stock_available_unreserved

View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# Copyright 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from openerp.tests.common import TransactionCase
class TestStockLogisticsWarehouse(TransactionCase):
def test01_stock_levels(self):
"""checking that qty_available_not_res actually reflects \
the variations in stock, both on product and template"""
pickingObj = self.env['stock.picking']
productObj = self.env['product.product']
templateObj = self.env['product.template']
supplier_location = self.env.ref('stock.stock_location_suppliers')
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
uom_unit = self.env.ref('product.product_uom_unit')
# Create product template
templateAB = templateObj.create(
{'name': 'templAB',
'uom_id': uom_unit.id,
})
# Create product A and B
productA = productObj.create(
{'name': 'product A',
'standard_price': 1,
'type': 'product',
'uom_id': uom_unit.id,
'default_code': 'A',
'product_tmpl_id': templateAB.id,
})
productB = productObj.create(
{'name': 'product B',
'standard_price': 1,
'type': 'product',
'uom_id': uom_unit.id,
'default_code': 'B',
'product_tmpl_id': templateAB.id,
})
# Create a picking move from INCOMING to STOCK
pickingInA = pickingObj.create({
'picking_type_id': self.ref('stock.picking_type_in'),
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': productA.id,
'product_uom': productA.uom_id.id,
'product_uom_qty': 2,
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
})]
})
pickingInB = pickingObj.create({
'picking_type_id': self.ref('stock.picking_type_in'),
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': productB.id,
'product_uom': productB.uom_id.id,
'product_uom_qty': 3,
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
})]
})
def compare_qty_available_not_res(product, value):
# Refresh, because the function field is not recalculated between
# transactions
product.refresh()
self.assertEqual(product.qty_available_not_res, value)
compare_qty_available_not_res(productA, 0)
compare_qty_available_not_res(templateAB, 0)
pickingInA.action_confirm()
compare_qty_available_not_res(productA, 0)
compare_qty_available_not_res(templateAB, 0)
pickingInA.action_assign()
compare_qty_available_not_res(productA, 0)
compare_qty_available_not_res(templateAB, 0)
pickingInA.action_done()
compare_qty_available_not_res(productA, 2)
compare_qty_available_not_res(templateAB, 2)
# will directly trigger action_done on productB
pickingInB.action_done()
compare_qty_available_not_res(productA, 2)
compare_qty_available_not_res(productB, 3)
compare_qty_available_not_res(templateAB, 5)
# Create a picking from STOCK to CUSTOMER
pickingOutA = pickingObj.create({
'picking_type_id': self.ref('stock.picking_type_out'),
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': productB.id,
'product_uom': productB.uom_id.id,
'product_uom_qty': 2,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})]
})
compare_qty_available_not_res(productB, 3)
compare_qty_available_not_res(templateAB, 5)
pickingOutA.action_confirm()
compare_qty_available_not_res(productB, 3)
compare_qty_available_not_res(templateAB, 5)
pickingOutA.action_assign()
compare_qty_available_not_res(productB, 1)
compare_qty_available_not_res(templateAB, 3)
pickingOutA.action_done()
compare_qty_available_not_res(productB, 1)
compare_qty_available_not_res(templateAB, 3)

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_stock_product_template_tree" model="ir.ui.view">
<field name="name">product.template.stock.tree.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id"
ref="stock.view_stock_product_template_tree"/>
<field name="arch" type="xml">
<field name="qty_available" position="after">
<field name="qty_available_not_res"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="product_template_kanban_stock_view">
<field name="name">Product Template Kanban Stock</field>
<field name="model">product.template</field>
<field name="inherit_id"
ref="stock.product_template_kanban_stock_view"/>
<field name="arch" type="xml">
<ul position="inside">
<li t-if="record.type.raw_value == 'product'">Unreserved: <field name="qty_available_not_res"/> <field name="uom_id"/></li>
</ul>
</field>
</record>
<record id="view_stock_product_tree" model="ir.ui.view">
<field name="name">product.stock.tree.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="stock.view_stock_product_tree"/>
<field name="arch" type="xml">
<field name="qty_available" position="after">
<field name="qty_available_not_res"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="product_template_form_view_procurement_button">
<field name="name">product.template_procurement</field>
<field name="model">product.template</field>
<field name="inherit_id"
ref="stock.product_template_form_view_procurement_button"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button type="object"
name="action_open_quants_unreserved"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-building-o">
<field name="qty_available_not_res" widget="statinfo"
string="Unreserved"/>
</button>
</div>
</field>
</record>
<record model="ir.ui.view" id="product_form_view_procurement_button">
<field name="name">product.product.procurement</field>
<field name="model">product.product</field>
<field name="inherit_id"
ref="stock.product_form_view_procurement_button"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button"
name="%(product_open_quants_unreserved)d"
icon="fa-building-o"
type="action" attrs="{'invisible':[('type', '!=', 'product')]}">
<field name="qty_available_not_res" widget="statinfo"
string="Unreserved"/>
</button>
</div>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="quant_search_view" model="ir.ui.view">
<field name="name">stock.quant.search</field>
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.quant_search_view"/>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<field name="owner_id" position="after">
<field name="reservation_id"/>
</field>
<filter name="internal_loc" position="after">
<filter name='internal_unreserved'
string="Internal Unreserved"
domain="[('reservation_id','=', False), ('location_id.usage','=', 'internal')]"/>
</filter>
</field>
</record>
<record model="ir.actions.act_window"
id="product_open_quants_unreserved">
<field name="name">Stock On Hand (Unreserved)</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_locationgroup':1}</field>
<field name="domain">[('product_id', '=', active_id), ('reservation_id', '=', False)]</field>
<field name="res_model">stock.quant</field>
</record>
</data>
</openerp>