Skip to content

Commit

Permalink
[ADD] new module 'stock_available_unreserved' (OCA#206)
Browse files Browse the repository at this point in the history
* [ADD] new module 'stock_available_unreserved'
  • Loading branch information
JordiBForgeFlow authored and gfcapalbo committed Jun 29, 2017
1 parent 13fbc1c commit de50b82
Show file tree
Hide file tree
Showing 10 changed files with 492 additions and 0 deletions.
64 changes: 64 additions & 0 deletions stock_available_unreserved/README.rst
@@ -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.
7 changes: 7 additions & 0 deletions stock_available_unreserved/__init__.py
@@ -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
21 changes: 21 additions & 0 deletions stock_available_unreserved/__openerp__.py
@@ -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,
}
7 changes: 7 additions & 0 deletions stock_available_unreserved/models/__init__.py
@@ -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
141 changes: 141 additions & 0 deletions stock_available_unreserved/models/product.py
@@ -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.
7 changes: 7 additions & 0 deletions stock_available_unreserved/tests/__init__.py
@@ -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 stock_available_unreserved/tests/test_stock_available_unreserved.py
@@ -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)

0 comments on commit de50b82

Please sign in to comment.