[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:
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user