Commit 7ac9c0b0 authored by Sebastien Robin's avatar Sebastien Robin

Inventory: add lowest_value_test for getInventoryAssetPrice

parent 7cef58c5
...@@ -45,19 +45,28 @@ order by d_year<dtml-if "'Monthly' in valuation_method">, d_month</dtml-if> ...@@ -45,19 +45,28 @@ order by d_year<dtml-if "'Monthly' in valuation_method">, d_month</dtml-if>
Very similar to (Monthly)WeightedAverage except that we do not have to Very similar to (Monthly)WeightedAverage except that we do not have to
split the timeframe / fold movements and simply perform a weighted average split the timeframe / fold movements and simply perform a weighted average
on all single movements. on all single movements.
Parameter lowest_value_test compares latest price and Moving average price and takes the
smallest one.
*/ */
set @total_asset_price=0, @total_quantity=0 SET @total_asset_price=0, @total_quantity=0, @latest_price=null, @running_total_asset_price=0
<dtml-var sql_delimiter> <dtml-var sql_delimiter>
select select
(@incoming_total_price:=IF(quantity>0, total_price, 0)) as incoming_total_price, (@incoming_total_price:=IF(quantity>0, total_price, 0)) as incoming_total_price,
@latest_price:=IF(quantity>0, total_price/quantity, @latest_price) as latest_price,
@unit_price:=((@total_asset_price+@incoming_total_price)/(@total_quantity+GREATEST(0, quantity))) as unit_price, @unit_price:=((@total_asset_price+@incoming_total_price)/(@total_quantity+GREATEST(0, quantity))) as unit_price,
(@total_asset_price:= (@running_total_asset_price:=
@total_asset_price + @running_total_asset_price +
@incoming_total_price + @incoming_total_price +
LEAST(0, quantity) * @unit_price) as total_asset_price, LEAST(0, quantity) * @unit_price) as running_total_asset_price,
(@total_quantity:=@total_quantity+quantity) as dummy (@total_quantity:=@total_quantity+quantity) as dummy,
<dtml-if "lowest_value_test">
(@total_asset_price:=LEAST(@running_total_asset_price, @total_quantity*@latest_price)) as total_asset_price
<dtml-else>
(@total_asset_price:= @running_total_asset_price) as total_asset_price
</dtml-if>
from from
stock, catalog stock, catalog
where where
...@@ -93,6 +102,9 @@ Thus, each movement has a value of: ...@@ -93,6 +102,9 @@ Thus, each movement has a value of:
if @unbalanced_output is initialized to @total_output_quantity and reduced by if @unbalanced_output is initialized to @total_output_quantity and reduced by
quantity at each step: quantity at each step:
unbalanced_output=max(0, unbalanced_output-quantity) unbalanced_output=max(0, unbalanced_output-quantity)
Parameter lowest_value_test compares latest price and FIFO price and takes the
smallest one.
*/ */
SET SET
@unbalanced_output:= @unbalanced_output:=
...@@ -105,16 +117,27 @@ SET ...@@ -105,16 +117,27 @@ SET
AND AND
<dtml-var where_expression> <dtml-var where_expression>
),0), ),0),
@total_asset_price=0 @total_asset_price=0,
@running_total_asset_price=0,
@running_quantity=0
<dtml-var sql_delimiter> <dtml-var sql_delimiter>
SELECT SELECT
(@total_asset_price:=@total_asset_price + (@running_quantity:=@running_quantity +
GREATEST(0, quantity-@unbalanced_output)
) AS running_quantity,
(@running_total_asset_price:=@running_total_asset_price +
GREATEST(0, (quantity-@unbalanced_output) * total_price/quantity) GREATEST(0, (quantity-@unbalanced_output) * total_price/quantity)
) AS total_asset_price, ) AS running_total_asset_price,
(@unbalanced_output:=GREATEST(0, @unbalanced_output-quantity)) as dummy (@unbalanced_output:=GREATEST(0, @unbalanced_output-quantity)) as dummy,
<dtml-if "lowest_value_test">
(@total_asset_price:=LEAST(@running_total_asset_price,
@running_quantity * total_price/quantity)) as total_asset_price
<dtml-else>
(@total_asset_price:= @running_total_asset_price) as total_asset_price
</dtml-if>
FROM FROM
stock, catalog stock, catalog
WHERE WHERE
...@@ -148,15 +171,26 @@ until we reach an incoming movement. Then: ...@@ -148,15 +171,26 @@ until we reach an incoming movement. Then:
movement got out of inventory between t=current and T=END. These items are not movement got out of inventory between t=current and T=END. These items are not
present in the final inventory and can be discarded. present in the final inventory and can be discarded.
@unbalanced_inventory=@unbalanced_inventory - quantity @unbalanced_inventory=@unbalanced_inventory - quantity
Parameter lowest_value_test compares latest price and FILO price and takes the
smallest one.
*/ */
SET @unbalanced_output=0, @total_asset_price=0 SET @unbalanced_output=0, @total_asset_price=0, @running_total_asset_price=0, @latest_price=null, @running_quantity=0
<dtml-var sql_delimiter> <dtml-var sql_delimiter>
SELECT SELECT
(@total_asset_price:=@total_asset_price + (IF(quantity <= 0, @latest_price, @latest_price:=IFNULL(@latest_price, total_price/quantity))) as dummy_latest_price,
(@running_total_asset_price:=@running_total_asset_price +
IF(quantity <= 0, 0, IF(quantity <= 0, 0,
total_price/quantity * GREATEST(0, quantity-@unbalanced_output))) as total_asset_price, total_price/quantity * GREATEST(0, quantity-@unbalanced_output))) as running_total_asset_price,
(@unbalanced_output:=GREATEST(0, @unbalanced_output-quantity)) as dummy (@unbalanced_output:=GREATEST(0, @unbalanced_output-quantity)) as dummy,
(@running_quantity:=@running_quantity + quantity) as running_quantity,
<dtml-if "lowest_value_test">
(@total_asset_price:=LEAST(@running_total_asset_price, @running_quantity*@latest_price)) as total_asset_price
<dtml-else>
(@total_asset_price:= @running_total_asset_price) as total_asset_price
</dtml-if>
FROM FROM
stock, catalog stock, catalog
WHERE WHERE
......
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
<item> <item>
<key> <string>arguments_src</string> </key> <key> <string>arguments_src</string> </key>
<value> <string>where_expression\n <value> <string>where_expression\n
valuation_method</string> </value> valuation_method\n
lowest_value_test\n
</string> </value>
</item> </item>
<item> <item>
<key> <string>connection_id</string> </key> <key> <string>connection_id</string> </key>
......
...@@ -1821,6 +1821,7 @@ class SimulationTool(BaseTool): ...@@ -1821,6 +1821,7 @@ class SimulationTool(BaseTool):
def getInventoryAssetPrice(self, src__=0, def getInventoryAssetPrice(self, src__=0,
simulation_period='', simulation_period='',
valuation_method=None, valuation_method=None,
lowest_value_test=False,
**kw): **kw):
""" """
Same thing as getInventory but returns an asset Same thing as getInventory but returns an asset
...@@ -1836,6 +1837,10 @@ class SimulationTool(BaseTool): ...@@ -1836,6 +1837,10 @@ class SimulationTool(BaseTool):
MovingAverage MovingAverage
When using a specific valuation method, a resource_uid is expected When using a specific valuation method, a resource_uid is expected
as well as one of (section_uid or node_uid). as well as one of (section_uid or node_uid).
Parameter lowest_value_price compares latest price with price
calculated with valuation_method and takes the smallest one. This
is useful for accountants to avoid overestimating the price of a stock.
""" """
if valuation_method is None: if valuation_method is None:
method = getattr(self,'get%sInventoryList' % simulation_period) method = getattr(self,'get%sInventoryList' % simulation_period)
...@@ -1857,6 +1862,9 @@ class SimulationTool(BaseTool): ...@@ -1857,6 +1862,9 @@ class SimulationTool(BaseTool):
if valuation_method not in ('Fifo', 'Filo', 'WeightedAverage', if valuation_method not in ('Fifo', 'Filo', 'WeightedAverage',
'MonthlyWeightedAverage', 'MovingAverage'): 'MonthlyWeightedAverage', 'MovingAverage'):
raise ValueError("Invalid valuation method: %s" % valuation_method) raise ValueError("Invalid valuation method: %s" % valuation_method)
if lowest_value_test and valuation_method not in ('Fifo', 'Filo',
'MovingAverage'):
raise NotImplementedError('lowest_value_test not implemented')
assert 'node_uid' in kw or 'section_uid' in kw assert 'node_uid' in kw or 'section_uid' in kw
sql_kw = self._generateSQLKeywordDict(**kw) sql_kw = self._generateSQLKeywordDict(**kw)
...@@ -1868,6 +1876,7 @@ class SimulationTool(BaseTool): ...@@ -1868,6 +1876,7 @@ class SimulationTool(BaseTool):
result = self.Resource_zGetAssetPrice( result = self.Resource_zGetAssetPrice(
valuation_method=valuation_method, valuation_method=valuation_method,
lowest_value_test=lowest_value_test,
src__=src__, src__=src__,
**sql_kw) **sql_kw)
......
...@@ -1516,20 +1516,22 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1516,20 +1516,22 @@ class TestInventoryList(InventoryAPITestCase):
def test_inventory_asset_price(self): def test_inventory_asset_price(self):
# examples from http://accountinginfo.com/study/inventory/inventory-120.htm # examples from http://accountinginfo.com/study/inventory/inventory-120.htm
# # total quantity
movement_list = [ movement_list = [
(1, "Beginning Inventory", -700, 10), (1, "Beginning Inventory", -700, 10), # 700
(3, "Purchase", -100, 12), (3, "Purchase", -100, 12), # 800
(8, "Sale", 500, None), (8, "Sale", 500, None), # 300
(15, "Purchase", -600, 14), (15, "Purchase", -600, 14), # 900
(19, "Purchase", -200, 15), (19, "Purchase", -200, 15), # 1100
(25, "Sale", 400, None), (25, "Sale", 400, None), # 700
(27, "Sale", 100, None), (27, "Sale", 100, None), # 600
] ]
resource = self.getProductModule().newContent( resource = self.getProductModule().newContent(
title='My resource', title='My resource',
portal_type='Product') portal_type='Product')
for m in movement_list: def makeMovementList(movement_list):
self._makeMovement(resource_value=resource, for m in movement_list:
self._makeMovement(resource_value=resource,
source_value=self.node, source_value=self.node,
destination_value=self.mirror_node, destination_value=self.mirror_node,
start_date=DateTime('2000/1/%d 12:00 UTC' % m[0]), start_date=DateTime('2000/1/%d 12:00 UTC' % m[0]),
...@@ -1537,19 +1539,51 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1537,19 +1539,51 @@ class TestInventoryList(InventoryAPITestCase):
quantity=m[2], quantity=m[2],
price=m[3], price=m[3],
) )
makeMovementList(movement_list)
simulation_tool = self.getSimulationTool() simulation_tool = self.getSimulationTool()
def valuate(method): def valuate(method, lowest_value_test=False):
self.portal.person_module.log(simulation_tool.getInventoryAssetPrice(
src__=1,
valuation_method=method,
resource_uid=resource.getUid(),
node_uid=self.node.getUid(),
lowest_value_test=lowest_value_test))
r = simulation_tool.getInventoryAssetPrice( r = simulation_tool.getInventoryAssetPrice(
valuation_method=method, valuation_method=method,
resource_uid=resource.getUid(), resource_uid=resource.getUid(),
node_uid=self.node.getUid()) node_uid=self.node.getUid(),
lowest_value_test=lowest_value_test)
return round(r) return round(r)
self.assertEqual(7895, valuate("MovingAverage")) self.assertEqual(7895, valuate("MovingAverage"))
self.assertEqual(7200, valuate("Filo")) self.assertEqual(7200, valuate("Filo"))
self.assertEqual(8600, valuate("Fifo")) self.assertEqual(8600, valuate("Fifo"))
# latest purchase price is 15, total quantity is 600
# average price of 13.15, thus lowest value test change nothing
self.assertEqual(7895, valuate("MovingAverage", lowest_value_test=True))
# average price of 12.15, thus lowest value test change nothing
self.assertEqual(7200, valuate("Filo", lowest_value_test=True))
# average price of 14.33, thus lowest value test change nothing
self.assertEqual(8600, valuate("Fifo", lowest_value_test=True))
movement_list = [
(28, "Purchase", -100, 11), # 700
(29, "Sale", 100, None), # 600
]
makeMovementList(movement_list)
self.assertEqual(7710, valuate("MovingAverage"))
self.assertEqual(7200, valuate("Filo"))
self.assertEqual(8300, valuate("Fifo"))
self.assertEqual(7710, valuate("MovingAverage"))
self.assertEqual(7200, valuate("Filo"))
# latest purchase price is 11, total quantity is 600
# average price of 12.85, thus lowest value test change value
self.assertEqual(6600, valuate("MovingAverage", lowest_value_test=True))
# average price of 12, thus lowest value test change value
self.assertEqual(6600, valuate("Filo", lowest_value_test=True))
# average price of 13.83, thus lowest value test change value
self.assertEqual(6600, valuate("Fifo", lowest_value_test=True))
def test_weighted_average_asset_price(self): def test_weighted_average_asset_price(self):
def h(quantity, total_price): def h(quantity, total_price):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment