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):
group_by_function_category=0,
group_by_function_category_strict_membership=0,
group_by_date=0,
group_by_time_interval_list=(),
# sort_on
sort_on=None,
group_by=None,
......@@ -980,6 +981,14 @@ class SimulationTool(BaseTool):
new_kw['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
#######################################################
......@@ -1189,6 +1198,7 @@ class SimulationTool(BaseTool):
group_by_section_category=0,
group_by_section_category_strict_membership=0,
group_by_resource=None,
group_by_time_interval_list=(),
group_by=None,
**ignored):
"""
......@@ -1208,7 +1218,8 @@ class SimulationTool(BaseTool):
group_by_function or group_by_mirror_section or group_by_payment 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_section_category_strict_membership:
group_by_section_category_strict_membership or \
group_by_time_interval_list:
if group_by_resource is None:
group_by_resource = 1
new_group_by_dict['group_by_resource'] = group_by_resource
......@@ -1315,7 +1326,8 @@ class SimulationTool(BaseTool):
# Get cached data
if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None 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
# from_date : >=
# to_date : <
......
<dtml-if group_by_time_interval_list>
SELECT slots.time_interval_index, q.* FROM (
</dtml-if>
SELECT
<dtml-if expr="precision is not None">
SUM(ROUND(<dtml-var stock_table_id>.quantity
......@@ -56,6 +59,8 @@ SELECT
COUNT(DISTINCT <dtml-var stock_table_id>.uid) AS stock_uid,
MAX(<dtml-var stock_table_id>.date) AS date
</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>
FROM
......@@ -69,6 +74,55 @@ FROM
</dtml-if>
</dtml-in>
, <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 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
......@@ -116,9 +170,28 @@ WHERE
<dtml-if group_by_expression>
GROUP BY
<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-if>
<dtml-if order_by_expression>
ORDER BY
<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>
......@@ -46,7 +46,8 @@ convert_quantity_result\n
quantity_unit_uid\n
stock_table_id=stock\n
transformed_uid\n
transformed_variation_text</string> </value>
transformed_variation_text\n
group_by_time_interval_list:list</string> </value>
</item>
<item>
<key> <string>cache_time_</string> </key>
......
......@@ -1267,6 +1267,232 @@ class TestInventoryList(InventoryAPITestCase):
self.assertEqual([r.inventory for r in inventory_list
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):
getInventoryList = self.getSimulationTool().getInventoryList
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