[IMP] stock: introducing free quantity on products

The stock module adds 4 quantities on products:
  Quantity on hand
  Incoming quantity
  Outgoing quantity
  and Forecast Quantity

Forecast Quantity is equals to Quantity on hand + Incoming quantity -
Outgoing quantity. Those two last ones are computed from the open stock
moves wainting to be validated but the reservation is not taken into account.
This commit introduces a fifth quantity 'free_qty' that acts like the forecast
one but only with reserved quantities.
This commit is contained in:
William Henrotin
2019-08-13 07:54:38 +00:00
parent 47bb7ab3f5
commit bbd8d31cdd
3 changed files with 43 additions and 5 deletions
+4
View File
@@ -88,6 +88,7 @@ class ProductProduct(models.Model):
- 'qty_available'
- 'incoming_qty'
- 'outgoing_qty'
- 'free_qty'
"""
for product in self:
bom_kit = self.env['mrp.bom']._bom_find(product=product, bom_type='phantom')
@@ -97,6 +98,7 @@ class ProductProduct(models.Model):
ratios_qty_available = []
ratios_incoming_qty = []
ratios_outgoing_qty = []
ratios_free_qty = []
for bom_line, bom_line_data in bom_sub_lines:
component = bom_line.product_id
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
@@ -105,11 +107,13 @@ class ProductProduct(models.Model):
ratios_qty_available.append(component.qty_available / qty_per_kit)
ratios_incoming_qty.append(component.incoming_qty / qty_per_kit)
ratios_outgoing_qty.append(component.outgoing_qty / qty_per_kit)
ratios_free_qty.append(component.free_qty / qty_per_kit)
if bom_sub_lines:
product.virtual_available = min(ratios_virtual_available) // 1
product.qty_available = min(ratios_qty_available) // 1
product.incoming_qty = min(ratios_incoming_qty) // 1
product.outgoing_qty = min(ratios_incoming_qty) // 1
product.free_qty = min(ratios_free_qty) // 1
else:
super(ProductProduct, self)._compute_quantities()
+23 -5
View File
@@ -47,6 +47,18 @@ class Product(models.Model):
"of its children.\n"
"Otherwise, this includes goods stored in any Stock Location "
"with 'internal' type.")
free_qty = fields.Float(
'Free To Use Quantity ', compute='_compute_quantities', search='_search_free_qty',
digits='Product Unit of Measure',
help="Forecast quantity (computed as Quantity On Hand "
"- reserved quantity)\n"
"In a context with a single Stock Location, this includes "
"goods stored in this location, or any of its children.\n"
"In a context with a single Warehouse, this includes "
"goods stored in the Stock Location of this Warehouse, or any "
"of its children.\n"
"Otherwise, this includes goods stored in any Stock Location "
"with 'internal' type.")
incoming_qty = fields.Float(
'Incoming', compute='_compute_quantities', search='_search_incoming_qty',
digits='Product Unit of Measure',
@@ -84,6 +96,7 @@ class Product(models.Model):
product.incoming_qty = res[product.id]['incoming_qty']
product.outgoing_qty = res[product.id]['outgoing_qty']
product.virtual_available = res[product.id]['virtual_available']
product.free_qty = res[product.id]['free_qty']
def _product_available(self, field_names=None, arg=False):
""" Compatibility method """
@@ -124,7 +137,7 @@ class Product(models.Model):
domain_move_out_todo = [('state', 'in', ('waiting', 'confirmed', 'assigned', 'partially_available'))] + domain_move_out
moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
quants_res = dict((item['product_id'][0], item['quantity']) for item in Quant.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id'))
quants_res = dict((item['product_id'][0], (item['quantity'], item['reserved_quantity'])) for item in Quant.read_group(domain_quant, ['product_id', 'quantity', 'reserved_quantity'], ['product_id'], orderby='id'))
if dates_in_the_past:
# Calculate the moves that were done before now to calculate back in time (as most questions will be recent ones)
domain_move_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done
@@ -137,17 +150,19 @@ class Product(models.Model):
product_id = product.id
if not product_id:
res[product_id] = dict.fromkeys(
['qty_available', 'incoming_qty', 'outgoing_qty', 'virtual_available'],
['qty_available', 'free_qty', 'incoming_qty', 'outgoing_qty', 'virtual_available'],
0.0,
)
continue
rounding = product.uom_id.rounding
res[product_id] = {}
if dates_in_the_past:
qty_available = quants_res.get(product_id, 0.0) - moves_in_res_past.get(product_id, 0.0) + moves_out_res_past.get(product_id, 0.0)
qty_available = quants_res.get(product_id, [0.0])[0] - moves_in_res_past.get(product_id, 0.0) + moves_out_res_past.get(product_id, 0.0)
else:
qty_available = quants_res.get(product_id, 0.0)
qty_available = quants_res.get(product_id, [0.0])[0]
reserved_quantity = quants_res.get(product_id, [False, 0.0])[1]
res[product_id]['qty_available'] = float_round(qty_available, precision_rounding=rounding)
res[product_id]['free_qty'] = float_round(qty_available - reserved_quantity, precision_rounding=rounding)
res[product_id]['incoming_qty'] = float_round(moves_in_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['outgoing_qty'] = float_round(moves_out_res.get(product_id, 0.0), precision_rounding=rounding)
res[product_id]['virtual_available'] = float_round(
@@ -297,10 +312,13 @@ class Product(models.Model):
# TDE FIXME: should probably clean the search methods
return self._search_product_quantity(operator, value, 'outgoing_qty')
def _search_free_qty(self, operator, value):
return self._search_product_quantity(operator, value, 'free_qty')
def _search_product_quantity(self, operator, value, field):
# TDE FIXME: should probably clean the search methods
# to prevent sql injections
if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'):
if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty', 'free_qty'):
raise UserError(_('Invalid domain left operand %s') % field)
if operator not in ('<', '>', '=', '!=', '<=', '>='):
raise UserError(_('Invalid domain operator %s') % operator)
+16
View File
@@ -65,3 +65,19 @@ class TestVirtualAvailable(TestStockCommon):
self.picking_out.action_assign()
self.picking_out_2.action_assign()
self.assertAlmostEqual(5.0, prod_context.virtual_available)
def test_free_quantity(self):
""" Test the value of product.free_qty. Free_qty = qty_on_hand - qty_reserved"""
self.assertAlmostEqual(40.0, self.product_3.free_qty)
self.picking_out.action_confirm()
self.picking_out_2.action_confirm()
# No reservation so free_qty is unchanged
self.assertAlmostEqual(40.0, self.product_3.free_qty)
self.picking_out.action_assign()
self.picking_out_2.action_assign()
# 8 units are now reserved
self.assertAlmostEqual(32.0, self.product_3.free_qty)
self.picking_out.do_unreserve()
self.picking_out_2.do_unreserve()
# 8 units are available again
self.assertAlmostEqual(40.0, self.product_3.free_qty)