Commit 9f1aa3bb authored by Jérome Perrin's avatar Jérome Perrin

SimulationTool: implement group_by_time_interval_list

parent 5016f968
...@@ -599,6 +599,7 @@ class SimulationTool(BaseTool): ...@@ -599,6 +599,7 @@ class SimulationTool(BaseTool):
group_by_function_category=0, group_by_function_category=0,
group_by_function_category_strict_membership=0, group_by_function_category_strict_membership=0,
group_by_date=0, group_by_date=0,
group_by_time_interval_list=(),
# sort_on # sort_on
sort_on=None, sort_on=None,
group_by=None, group_by=None,
...@@ -980,6 +981,14 @@ class SimulationTool(BaseTool): ...@@ -980,6 +981,14 @@ class SimulationTool(BaseTool):
new_kw['related_key_select_expression_list'] =\ new_kw['related_key_select_expression_list'] =\
related_key_select_expression_list related_key_select_expression_list
for slot_index, time_sequence in enumerate(group_by_time_interval_list):
if not (time_sequence.get('from_date') or time_sequence.get('at_date') or time_sequence.get('to_date')):
raise ValueError(
"Invalid time sequence {slot_index}: {time_sequence!r}".format(
slot_index=slot_index,
time_sequence=time_sequence,
))
sql_kw['group_by_time_interval_list'] = group_by_time_interval_list
return sql_kw, new_kw return sql_kw, new_kw
####################################################### #######################################################
...@@ -1189,6 +1198,7 @@ class SimulationTool(BaseTool): ...@@ -1189,6 +1198,7 @@ class SimulationTool(BaseTool):
group_by_section_category=0, group_by_section_category=0,
group_by_section_category_strict_membership=0, group_by_section_category_strict_membership=0,
group_by_resource=None, group_by_resource=None,
group_by_time_interval_list=(),
group_by=None, group_by=None,
**ignored): **ignored):
""" """
...@@ -1208,7 +1218,8 @@ class SimulationTool(BaseTool): ...@@ -1208,7 +1218,8 @@ class SimulationTool(BaseTool):
group_by_function or group_by_mirror_section or group_by_payment or \ group_by_function or group_by_mirror_section or group_by_payment or \
group_by_sub_variation or group_by_variation or \ group_by_sub_variation or group_by_variation or \
group_by_movement or group_by_date or group_by_section_category or\ group_by_movement or group_by_date or group_by_section_category or\
group_by_section_category_strict_membership: group_by_section_category_strict_membership or \
group_by_time_interval_list:
if group_by_resource is None: if group_by_resource is None:
group_by_resource = 1 group_by_resource = 1
new_group_by_dict['group_by_resource'] = group_by_resource new_group_by_dict['group_by_resource'] = group_by_resource
...@@ -1315,7 +1326,8 @@ class SimulationTool(BaseTool): ...@@ -1315,7 +1326,8 @@ class SimulationTool(BaseTool):
# Get cached data # Get cached data
if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None and \ if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None and \
optimisation__ and (not kw.get('from_date')) and \ optimisation__ and (not kw.get('from_date')) and \
'transformed_resource' not in kw: 'transformed_resource' not in kw \
and "group_by_time_interval_list" not in kw:
# Here is the different kind of date # Here is the different kind of date
# from_date : >= # from_date : >=
# to_date : < # to_date : <
......
<dtml-if group_by_time_interval_list>
SELECT slots.time_interval_index, q.* FROM (
</dtml-if>
SELECT SELECT
<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-var stock_table_id>.quantity
...@@ -56,6 +59,8 @@ SELECT ...@@ -56,6 +59,8 @@ SELECT
COUNT(DISTINCT <dtml-var stock_table_id>.uid) AS stock_uid, COUNT(DISTINCT <dtml-var stock_table_id>.uid) AS stock_uid,
MAX(<dtml-var stock_table_id>.date) AS date MAX(<dtml-var stock_table_id>.date) AS date
</dtml-if> </dtml-if>
<dtml-if group_by_time_interval_list>, time_interval_index as _time_interval_index</dtml-if>
<dtml-if select_expression>, <dtml-var select_expression></dtml-if> <dtml-if select_expression>, <dtml-var select_expression></dtml-if>
FROM FROM
...@@ -69,6 +74,55 @@ FROM ...@@ -69,6 +74,55 @@ FROM
</dtml-if> </dtml-if>
</dtml-in> </dtml-in>
, <dtml-var stock_table_id> , <dtml-var stock_table_id>
<dtml-if group_by_time_interval_list>
RIGHT JOIN
( <dtml-in prefix="time_interval" expr="_.list(_.enumerate(group_by_time_interval_list))">
SELECT
<dtml-sqlvar expr="time_interval_key" type="int"> time_interval_index,
<dtml-sqlvar expr="time_interval_item.get('from_date')" type="datetime" optional> time_interval_from_date,
<dtml-sqlvar expr="time_interval_item.get('at_date')" type="datetime" optional> time_interval_at_date,
<dtml-sqlvar expr="time_interval_item.get('to_date')" type="datetime" optional> time_interval_to_date
<dtml-unless time_interval_end>UNION ALL</dtml-unless>
</dtml-in> ) slots
ON
<dtml-if group_by_time_interval_list>
(
( time_interval_from_date is not null AND
( time_interval_at_date is not null AND
GREATEST(`stock`.`date`, `stock`.`mirror_date`) >= time_interval_from_date AND
LEAST(`stock`.`date`, `stock`.`mirror_date`) <= time_interval_at_date
) OR (
(
time_interval_to_date is not null AND
GREATEST(`stock`.`date`, `stock`.`mirror_date`) >= time_interval_from_date AND
LEAST(`stock`.`date`, `stock`.`mirror_date`) < time_interval_to_date
) OR (
GREATEST(`stock`.`date`, `stock`.`mirror_date`) >= time_interval_from_date AND
time_interval_at_date is null AND time_interval_to_date is null
)
)
) OR (
time_interval_from_date is null AND (
( time_interval_at_date is not null AND
( LEAST(`stock`.`date`, `stock`.`mirror_date`) <= time_interval_at_date )
) OR LEAST(`stock`.`date`, `stock`.`mirror_date`) < time_interval_to_date
)
)
)
<dtml-else>
(
( time_interval_from_date is null OR stock.date >= time_interval_from_date )
AND ( time_interval_at_date is null OR stock.date <= time_interval_at_date )
AND ( time_interval_to_date is null OR stock.date < time_interval_to_date )
)
</dtml-if>
</dtml-if>
</dtml-if> </dtml-if>
<dtml-if quantity_unit_uid> <dtml-comment>XXX quantity unit conversion will not work when using implict_join=False</dtml-comment> <dtml-if quantity_unit_uid> <dtml-comment>XXX quantity unit conversion will not work when using implict_join=False</dtml-comment>
LEFT JOIN quantity_unit_conversion ON LEFT JOIN quantity_unit_conversion ON
...@@ -116,9 +170,28 @@ WHERE ...@@ -116,9 +170,28 @@ WHERE
<dtml-if group_by_expression> <dtml-if group_by_expression>
GROUP BY GROUP BY
<dtml-if transformed_uid>transformation.transformed_uid,</dtml-if> <dtml-if transformed_uid>transformation.transformed_uid,</dtml-if>
<dtml-if group_by_time_interval_list>time_interval_index,</dtml-if>
<dtml-var group_by_expression> <dtml-var group_by_expression>
</dtml-if> </dtml-if>
<dtml-if order_by_expression> <dtml-if order_by_expression>
ORDER BY ORDER BY
<dtml-var order_by_expression> <dtml-var order_by_expression>
<dtml-else>
<dtml-if group_by_time_interval_list>
ORDER BY time_interval_index
</dtml-if>
</dtml-if>
<dtml-if group_by_time_interval_list>
) q
RIGHT JOIN
( <dtml-in prefix="time_interval" expr="_.list(_.enumerate(group_by_time_interval_list))">
SELECT
<dtml-sqlvar expr="time_interval_key" type="int"> time_interval_index,
<dtml-sqlvar expr="time_interval_item.get('from_date')" type="datetime" optional> time_interval_from_date,
<dtml-sqlvar expr="time_interval_item.get('at_date')" type="datetime" optional> time_interval_at_date,
<dtml-sqlvar expr="time_interval_item.get('to_date')" type="datetime" optional> time_interval_to_date
<dtml-unless time_interval_end>UNION ALL</dtml-unless>
</dtml-in> ) slots ON (q._time_interval_index = slots.time_interval_index)
</dtml-if> </dtml-if>
...@@ -46,7 +46,8 @@ convert_quantity_result\n ...@@ -46,7 +46,8 @@ convert_quantity_result\n
quantity_unit_uid\n quantity_unit_uid\n
stock_table_id=stock\n stock_table_id=stock\n
transformed_uid\n transformed_uid\n
transformed_variation_text</string> </value> transformed_variation_text\n
group_by_time_interval_list:list</string> </value>
</item> </item>
<item> <item>
<key> <string>cache_time_</string> </key> <key> <string>cache_time_</string> </key>
......
...@@ -1267,6 +1267,232 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1267,6 +1267,232 @@ class TestInventoryList(InventoryAPITestCase):
self.assertEqual([r.inventory for r in inventory_list self.assertEqual([r.inventory for r in inventory_list
if r.strict_use_uid == use.use1.use12.getUid()], [11]) if r.strict_use_uid == use.use1.use12.getUid()], [11])
def test_group_by_time_interval(self):
getInventoryList = self.getSimulationTool().getInventoryList
# Create 3 groups of movements:
self._makeMovement(quantity=1, start_date=DateTime('2016/01/01'))
self._makeMovement(quantity=3, start_date=DateTime('2016/02/01'))
self._makeMovement(quantity=5, start_date=DateTime('2016/02/02'))
self._makeMovement(quantity=7, start_date=DateTime('2016/03/01'))
# Create "noise" movement that we should not select
self._makeMovement(
quantity=10,
start_date=DateTime('2016/02/01'),
destination_value=self.portal.organisation_module.newContent())
inventory_list = getInventoryList(
node_uid=self.node.getUid(),
group_by_time_interval_list=[
{
'at_date': DateTime('2016/01/01').latestTime()
},
{
'from_date': DateTime('2016/02/01'),
'to_date': DateTime('2016/03/01')
},
{
'from_date': DateTime('2016/03/01')
},
])
# by default, time sequence are returned sorted by keys.
self.assertEqual(3, len(inventory_list))
self.assertEqual(1, inventory_list[0].total_quantity)
self.assertEqual(0, inventory_list[0].time_interval_index)
self.assertEqual(3 + 5, inventory_list[1].total_quantity)
self.assertEqual(1, inventory_list[1].time_interval_index)
self.assertEqual(7, inventory_list[2].total_quantity)
self.assertEqual(2, inventory_list[2].time_interval_index)
# now using all combinations of from_date, at_date & to_date
inventory_list = getInventoryList(
node_uid=self.node.getUid(),
group_by_time_interval_list=[
{
'at_date': DateTime('2016/01/01').latestTime()
},
{
'to_date': DateTime('2016/01/02')
}, # equivalent to above
{
'from_date': DateTime('2016/02/01'),
'at_date': DateTime('2016/02/29').latestTime()
},
{
'from_date': DateTime('2016/02/01'),
'to_date': DateTime('2016/03/01')
},
{
'from_date': DateTime('2016/03/01')
},
])
self.assertEqual(
[1, 1, 3 + 5, 3 + 5, 7],
[brain.inventory for brain in inventory_list],
)
def test_group_by_time_interval_empty_slots_are_returned(self):
getInventoryList = self.getSimulationTool().getInventoryList
self._makeMovement(
title="M1", quantity=3, start_date=DateTime('2016/01/01'))
self._makeMovement(
title="M2", quantity=5, start_date=DateTime('2016/02/01'))
inventory_list = getInventoryList(
node_uid=self.node.getUid(),
group_by_time_interval_list=[
# before M1 -> empty
{
'at_date': DateTime('2001/01/01').latestTime()
},
{
'to_date': DateTime('2001/01/01')
},
{
'from_date': DateTime('1999/01/01'),
'to_date': DateTime('2001/01/01')
},
{
'from_date': DateTime('1999/01/01'),
'at_date': DateTime('2001/01/01')
},
# selecting M1
{
'from_date': DateTime('2016/01/01'),
'to_date': DateTime('2016/01/02')
},
# between M1 & M2 -> empty
{
'from_date': DateTime('2016/01/02'),
'at_date': DateTime('2001/01/03')
},
{
'from_date': DateTime('2016/01/02'),
'to_date': DateTime('2001/01/03')
},
# selecting M2
{
'from_date': DateTime('2016/02/01'),
'to_date': DateTime('2016/02/03')
},
# after M2 -> empty
{
'from_date': DateTime('2016/02/03'),
'to_date': DateTime('2016/02/04')
},
{
'from_date': DateTime('2016/02/03'),
'at_date': DateTime('2001/02/04')
},
{
'from_date': DateTime('2016/02/03')
},
])
self.assertEqual(
[
None,
None,
None,
None,
3,
None,
None,
5,
None,
None,
None,
],
[x.inventory for x in inventory_list],
)
def test_group_by_time_interval_and_other_group_by(self):
# group_by_time_interval_list can be used with other "group by" parameters
getInventoryList = self.getSimulationTool().getInventoryList
another_resource = self._makeResource()
self._makeMovement(
title="M1", quantity=5, start_date=DateTime('2016/01/01'))
self._makeMovement(
title="M2", quantity=7, start_date=DateTime('2016/01/03'))
self._makeMovement(
title="M3",
quantity=11,
resource_value=another_resource,
start_date=DateTime('2016/01/03'))
self.assertEqual(
{
(0, self.resource.uid): 5,
(1, self.resource.uid): 7,
(1, another_resource.uid): 11,
},
{(brain.time_interval_index, brain.resource_uid): brain.inventory
for brain in getInventoryList(
node_uid=self.node.getUid(),
group_by_resource=True,
group_by_time_interval_list=[
{
'at_date': DateTime('2016/01/02')
},
{
'from_date': DateTime('2016/01/02')
},
])},
)
def test_group_by_time_interval_invalid_inputs(self):
getInventoryList = self.getSimulationTool().getInventoryList
self._makeMovement(
title="M1", quantity=3, start_date=DateTime('2016/01/02'))
# no from_date, at_date or to_date on a slot raise a ValueError
with self.assertRaises(ValueError):
getInventoryList(
node_uid=self.node.getUid(),
group_by_time_interval_list=[{}],
)
# intervals where start_date > stop_date are valid, but select nothing
self.assertEqual(
{0: None},
{
brain.time_interval_index: brain.inventory
for brain in getInventoryList(
node_uid=self.node.getUid(),
group_by_time_interval_list=[{
'from_date': DateTime('2016/01/03'),
'at_date': DateTime('2016/01/01')
}])
},
)
self.assertEqual(
{0: None},
{
brain.time_interval_index: brain.inventory
for brain in getInventoryList(
node_uid=self.node.getUid(),
group_by_time_interval_list=[{
'from_date': DateTime('2016/01/03'),
'to_date': DateTime('2016/01/01')
}])
},
)
def test_OmitInputOmitOutput(self): def test_OmitInputOmitOutput(self):
getInventoryList = self.getSimulationTool().getInventoryList getInventoryList = self.getSimulationTool().getInventoryList
self._makeMovement(quantity=1, price=1) self._makeMovement(quantity=1, price=1)
......
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