forked from OCA/stock-logistics-warehouse
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ADD] new module 'stock_available_unreserved' (OCA#206)
* [ADD] new module 'stock_available_unreserved'
- Loading branch information
1 parent
13fbc1c
commit de50b82
Showing
10 changed files
with
492 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
136 changes: 136 additions & 0 deletions
136
stock_available_unreserved/tests/test_stock_available_unreserved.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Oops, something went wrong.