Commit d734d144 authored by Jérome Perrin's avatar Jérome Perrin

SimulationTool: implement "linear" flow API

Conflicts:
	product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetInventoryList.xml
parent 83b096c4
...@@ -559,6 +559,8 @@ class SimulationTool(BaseTool): ...@@ -559,6 +559,8 @@ class SimulationTool(BaseTool):
omit_output=0, omit_output=0,
omit_asset_increase=0, omit_asset_increase=0,
omit_asset_decrease=0, omit_asset_decrease=0,
# interpolation_method
interpolation_method='default',
# group by # group by
group_by_node=0, group_by_node=0,
group_by_node_category=0, group_by_node_category=0,
...@@ -645,6 +647,24 @@ class SimulationTool(BaseTool): ...@@ -645,6 +647,24 @@ class SimulationTool(BaseTool):
date_dict['range'] = 'ngt' date_dict['range'] = 'ngt'
if date_dict: if date_dict:
column_value_dict['date'] = date_dict column_value_dict['date'] = date_dict
if interpolation_method != 'default':
assert from_date and to_date
# if we consider flow, we also select movement whose mirror date is
# in the from_date/to_date range and movement whose
# start_date/stop_date contains the report range.
column_value_dict['date'] = ComplexQuery(
Query(date=(from_date, to_date), range='minmax'),
Query(mirror_date=(from_date, to_date), range='minmax'),
ComplexQuery(
Query(mirror_date=from_date, range='min'),
Query(date=to_date, range='max'),
operator="AND"),
ComplexQuery(
Query(date=from_date, range='min'),
Query(mirror_date=to_date, range='max'),
operator="AND"),
operator="OR"
)
else: else:
column_value_dict['date'] = {'query': [to_date], 'range': 'ngt'} column_value_dict['date'] = {'query': [to_date], 'range': 'ngt'}
column_value_dict['mirror_date'] = {'query': [from_date], 'range': 'nlt'} column_value_dict['mirror_date'] = {'query': [from_date], 'range': 'nlt'}
...@@ -1011,6 +1031,12 @@ class SimulationTool(BaseTool): ...@@ -1011,6 +1031,12 @@ class SimulationTool(BaseTool):
output_simulation_state - only take rows with specified simulation_state output_simulation_state - only take rows with specified simulation_state
and quantity < 0 and quantity < 0
interpolation_method - XXX name ???? how to evaluate flow movement.
* full (default): Consider the movement decreases 100% of source stock on
start date and increase 100% of the destination node stock on stop date.
* linear: consider the movement decreases source stock and increase
destination stock linearly between start date and stop date.
ignore_variation - do not take into account variation in inventory ignore_variation - do not take into account variation in inventory
calculation (useless on getInventory, but useful on calculation (useless on getInventory, but useful on
getInventoryList) getInventoryList)
...@@ -1185,6 +1211,7 @@ class SimulationTool(BaseTool): ...@@ -1185,6 +1211,7 @@ class SimulationTool(BaseTool):
omit_simulation=0, omit_simulation=0,
only_accountable=True, only_accountable=True,
default_stock_table='stock', default_stock_table='stock',
interpolation_method='default',
selection_domain=None, selection_report=None, selection_domain=None, selection_report=None,
statistic=0, inventory_list=1, statistic=0, inventory_list=1,
precision=None, connection_id=None, precision=None, connection_id=None,
...@@ -1263,6 +1290,7 @@ class SimulationTool(BaseTool): ...@@ -1263,6 +1290,7 @@ class SimulationTool(BaseTool):
'stock_table_id': default_stock_table, 'stock_table_id': default_stock_table,
'src__': src__, 'src__': src__,
'ignore_variation': ignore_variation, 'ignore_variation': ignore_variation,
'interpolation_method': interpolation_method,
'standardise': standardise, 'standardise': standardise,
'omit_simulation': omit_simulation, 'omit_simulation': omit_simulation,
'only_accountable': only_accountable, 'only_accountable': only_accountable,
...@@ -1308,7 +1336,7 @@ class SimulationTool(BaseTool): ...@@ -1308,7 +1336,7 @@ class SimulationTool(BaseTool):
kw['from_date'] = cached_date kw['from_date'] = cached_date
else: else:
cached_result = [] cached_result = []
sql_kw, new_kw = self._generateKeywordDict(**kw) sql_kw, new_kw = self._generateKeywordDict(interpolation_method=interpolation_method, **kw)
# Copy kw content as _generateSQLKeywordDictFromKeywordDict # Copy kw content as _generateSQLKeywordDictFromKeywordDict
# remove some values from it # remove some values from it
try: try:
...@@ -1321,6 +1349,22 @@ class SimulationTool(BaseTool): ...@@ -1321,6 +1349,22 @@ class SimulationTool(BaseTool):
stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict( stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict(
table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw_copy) table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw_copy)
stock_sql_kw.update(base_inventory_kw) stock_sql_kw.update(base_inventory_kw)
# TODO: move in _generateSQLKeywordDictFromKeywordDict
if interpolation_method == 'linear':
# XXX only DateTime instance are supported
from_date = kw.get('from_date')
if from_date:
from_date = from_date.toZone("UTC")
to_date = kw.get('to_date')
if to_date:
to_date = to_date.toZone("UTC")
# TODO: support at_date ?
stock_sql_kw['interpolation_method_from_date'] = from_date
stock_sql_kw['interpolation_method_to_date'] = to_date
elif interpolation_method != 'default':
raise ValueError("Unsupported interpolation_method %r" % (interpolation_method,))
delta_result = self.Resource_zGetInventoryList( delta_result = self.Resource_zGetInventoryList(
**stock_sql_kw) **stock_sql_kw)
if src__: if src__:
......
SELECT SELECT
<dtml-if expr="interpolation_method == 'linear'">
@interpolation_ratio := CASE
WHEN <dtml-var stock_table_id>.mirror_date = <dtml-var stock_table_id>.date THEN 1
ELSE (
UNIX_TIMESTAMP(LEAST(<dtml-sqlvar flow_valuation_method_to_date type="datetime">,
GREATEST(<dtml-var stock_table_id>.date, <dtml-var stock_table_id>.mirror_date) ))
- UNIX_TIMESTAMP(GREATEST(<dtml-sqlvar flow_valuation_method_from_date type="datetime">,
LEAST(<dtml-var stock_table_id>.date, <dtml-var stock_table_id>.mirror_date))))
/ ( UNIX_TIMESTAMP(GREATEST(<dtml-var stock_table_id>.date, <dtml-var stock_table_id>.mirror_date)) -
UNIX_TIMESTAMP(LEAST(<dtml-var stock_table_id>.date, <dtml-var stock_table_id>.mirror_date)) ) END
<dtml-else>
@interpolation_ratio := 1
</dtml-if> flow_ratio,
<dtml-if expr="precision is not None"> <dtml-if expr="precision is not None">
SUM(ROUND(<dtml-var stock_table_id>.quantity SUM(ROUND(
<dtml-if transformed_uid> * transformation.quantity</dtml-if>, <dtml-var precision>)) AS inventory, <dtml-var stock_table_id>.quantity
SUM(ROUND(<dtml-var stock_table_id>.quantity <dtml-if transformed_uid> * transformation.quantity</dtml-if>
<dtml-if transformed_uid> * transformation.quantity</dtml-if>, <dtml-var precision>)) AS total_quantity, * @interpolation_ratio, <dtml-var precision>)) AS inventory,
SUM(ROUND(
<dtml-var stock_table_id>.quantity
<dtml-if transformed_uid> * transformation.quantity</dtml-if>
* @interpolation_ratio, <dtml-var precision>)) AS total_quantity,
<dtml-if convert_quantity_result> <dtml-if convert_quantity_result>
SUM(ROUND(<dtml-var stock_table_id>.quantity * measure.quantity SUM(ROUND(<dtml-var stock_table_id>.quantity * measure.quantity
<dtml-if quantity_unit_uid> / quantity_unit_conversion.quantity</dtml-if> <dtml-if quantity_unit_uid> / quantity_unit_conversion.quantity</dtml-if>
<dtml-if transformed_uid> * transformation.quantity</dtml-if>, <dtml-var precision>)) * @interpolation_ratio, <dtml-var precision>))
AS converted_quantity, AS converted_quantity,
</dtml-if> </dtml-if>
IFNULL(SUM(ROUND(<dtml-var stock_table_id>.total_price, <dtml-var precision>)), 0) AS total_price
IFNULL(SUM(ROUND(
<dtml-var stock_table_id>.total_price * @interpolation_ratio, <dtml-var precision>)), 0) AS total_price
<dtml-else> <dtml-else>
SUM(<dtml-var stock_table_id>.quantity <dtml-if transformed_uid> * transformation.quantity</dtml-if>) AS inventory, SUM(<dtml-var stock_table_id>.quantity
SUM(<dtml-var stock_table_id>.quantity <dtml-if transformed_uid> * transformation.quantity</dtml-if>) AS total_quantity, <dtml-if transformed_uid> * transformation.quantity</dtml-if>
* @interpolation_ratio
) AS inventory,
SUM(<dtml-var stock_table_id>.quantity
<dtml-if transformed_uid> * transformation.quantity</dtml-if>
* @interpolation_ratio
) AS total_quantity,
<dtml-if convert_quantity_result> <dtml-if convert_quantity_result>
ROUND(SUM(<dtml-var stock_table_id>.quantity * measure.quantity ROUND(SUM(<dtml-var stock_table_id>.quantity * measure.quantity
<dtml-if quantity_unit_uid> / quantity_unit_conversion.quantity</dtml-if> <dtml-if quantity_unit_uid> / quantity_unit_conversion.quantity</dtml-if>
<dtml-if transformed_uid> * transformation.quantity</dtml-if>), 12) <dtml-if transformed_uid> * transformation.quantity</dtml-if> * @flow_ratio), 12)
AS converted_quantity, AS converted_quantity,
</dtml-if> </dtml-if>
IFNULL(SUM(<dtml-var stock_table_id>.total_price), 0) AS total_price IFNULL(SUM(<dtml-var stock_table_id>.total_price) * @flow_ratio, 0) AS total_price
</dtml-if> </dtml-if>
<dtml-if inventory_list> <dtml-if inventory_list>
, ,
......
...@@ -45,7 +45,11 @@ convert_quantity_result\r\n ...@@ -45,7 +45,11 @@ convert_quantity_result\r\n
quantity_unit_uid\r\n quantity_unit_uid\r\n
stock_table_id=stock\r\n stock_table_id=stock\r\n
transformed_uid\r\n transformed_uid\r\n
transformed_variation_text</string> </value> transformed_variation_text\r\n
interpolation_method\r\n
interpolation_method_from_date\r\n
interpolation_method_to_date\r\n
interpolation_method_at_date</string> </value>
</item> </item>
<item> <item>
<key> <string>cache_time_</string> </key> <key> <string>cache_time_</string> </key>
......
...@@ -816,6 +816,204 @@ class TestInventory(InventoryAPITestCase): ...@@ -816,6 +816,204 @@ class TestInventory(InventoryAPITestCase):
resource=self.resource.getRelativeUrl(), resource=self.resource.getRelativeUrl(),
at_date=date_gmt_1) at_date=date_gmt_1)
def test_interpolation_method_linear(self):
self._makeMovement(
quantity=10,
start_date=DateTime("2016/01/01 01:00:00"),
stop_date=DateTime("2016/01/01 11:00:00"),
)
# With a time frame that does not contain the movement, we have 0%
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/02/01 00:00:00"),
to_date=DateTime("2016/02/02 00:00:00"),
interpolation_method='linear')
# With a time frame that contains the full movement, we have 100% of the quantity
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 00:00:00"),
to_date=DateTime("2016/01/02 00:00:00"),
interpolation_method='linear')
# corner case: exact same time, we also have 100%
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 01:00:00"),
to_date=DateTime("2016/01/01 11:00:00"),
interpolation_method='linear')
# With a time frame containing the 50% of the movement, we have 50% of the quantity
# time frame start before movement
self.assertInventoryEquals(5,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 00:00:00"),
to_date=DateTime("2016/01/01 06:00:00"),
interpolation_method='linear')
# time frame start at exact same time as movement
self.assertInventoryEquals(5,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 01:00:00"),
to_date=DateTime("2016/01/01 06:00:00"),
interpolation_method='linear')
# Time frame is contained inside the movement
self.assertInventoryEquals(5,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 02:00:00"),
to_date=DateTime("2016/01/01 07:00:00"),
interpolation_method='linear')
# Time frame finishes after movement end
self.assertInventoryEquals(5,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 06:00:00"),
to_date=DateTime("2016/01/01 12:00:00"),
interpolation_method='linear')
# Time frame finishes at exact same time that movement end
self.assertInventoryEquals(5,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 06:00:00"),
to_date=DateTime("2016/01/01 11:00:00"),
interpolation_method='linear')
def test_interpolation_method_XXX_one_for_all(self):
self._makeMovement(
quantity=10,
start_date=DateTime("2016/01/01 01:00:00"),
stop_date=DateTime("2016/01/01 11:00:00"),
)
# With a time frame that does not contain the movement, we have 0%
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/02/01 00:00:00"),
to_date=DateTime("2016/02/02 00:00:00"),
interpolation_method='one_for_all')
# With a time frame that contains the full movement, we have 100% of the quantity
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 00:00:00"),
to_date=DateTime("2016/01/02 00:00:00"),
interpolation_method='one_for_all')
# corner case: exact same time, we also have 100%
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 01:00:00"),
to_date=DateTime("2016/01/01 11:00:00"),
interpolation_method='one_for_all')
# With a time frame containing the 50% of the movement, we have 100% of the quantity
# this is "one_for_all" XXX naming
# time frame start before movement
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 00:00:00"),
to_date=DateTime("2016/01/01 06:00:00"),
interpolation_method='one_for_all')
# time frame start at exact same time as movement
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 01:00:00"),
to_date=DateTime("2016/01/01 06:00:00"),
interpolation_method='one_for_all')
# Time frame is contained inside the movement
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 02:00:00"),
to_date=DateTime("2016/01/01 07:00:00"),
interpolation_method='one_for_all')
# Time frame finishes after movement end
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 06:00:00"),
to_date=DateTime("2016/01/01 12:00:00"),
interpolation_method='one_for_all')
# Time frame finishes at exact same time that movement end
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 06:00:00"),
to_date=DateTime("2016/01/01 11:00:00"),
interpolation_method='one_for_all')
def test_interpolation_method_XXX_all_or_nothing(self):
self._makeMovement(
quantity=10,
start_date=DateTime("2016/01/01 01:00:00"),
stop_date=DateTime("2016/01/01 11:00:00"),
)
# With a time frame that does not contain the movement, we have 0%
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/02/01 00:00:00"),
to_date=DateTime("2016/02/02 00:00:00"),
interpolation_method='all_or_nothing')
# With a time frame that contains the full movement, we have 100% of the quantity
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 00:00:00"),
to_date=DateTime("2016/01/02 00:00:00"),
interpolation_method='all_or_nothing')
# corner case: exact same time, we also have 100%
self.assertInventoryEquals(10,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 01:00:00"),
to_date=DateTime("2016/01/01 11:00:00"),
interpolation_method='all_or_nothing')
# With a time frame containing the 50% of the movement, we have 0% of the quantity
# this is "all or nothing" XXX naming
# time frame start before movement
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 00:00:00"),
to_date=DateTime("2016/01/01 06:00:00"),
interpolation_method='all_or_nothing')
# time frame start at exact same time as movement
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 01:00:00"),
to_date=DateTime("2016/01/01 06:00:00"),
interpolation_method='all_or_nothing')
# Time frame is contained inside the movement
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 02:00:00"),
to_date=DateTime("2016/01/01 07:00:00"),
interpolation_method='all_or_nothing')
# Time frame finishes after movement end
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 06:00:00"),
to_date=DateTime("2016/01/01 12:00:00"),
interpolation_method='all_or_nothing')
# Time frame finishes at exact same time that movement end
self.assertInventoryEquals(0,
node_uid=self.node.getUid(),
from_date=DateTime("2016/01/01 06:00:00"),
to_date=DateTime("2016/01/01 11:00:00"),
interpolation_method='all_or_nothing')
class TestInventoryList(InventoryAPITestCase): class TestInventoryList(InventoryAPITestCase):
"""Tests getInventoryList methods. """Tests getInventoryList methods.
""" """
......
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