Commit cf53db45 authored by Aurel's avatar Aurel

Implement cache of getInventory results

Create a sql cache for getInventory call
Remove full-inventory optimisation as it now useless
Implement full-inventory feature directly into stock
Make erp5_pdm depends on this optimisation
Install optimisation for all unit tests
parent 3f484980
<catalog_method>
<item key="sql_catalog_object_list" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>isInventory\r\n
uid\r\n
getDestinationUid\r\n
getDestinationSectionUid\r\n
getDestinationPaymentUid\r\n
getStartDate\r\n
isFullInventory\r\n
getSimulationState</string> </value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_catalog_inventory_list</string> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
<dtml-let row_list="[]">\n
\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
<dtml-if expr="isInventory[loop_item]">\n
<dtml-if expr="getStartDate[loop_item] is not None">\n
<dtml-call expr="row_list.append((\n
uid[loop_item],\n
getDestinationUid[loop_item],\n
getDestinationSectionUid[loop_item],\n
getDestinationPaymentUid[loop_item],\n
getStartDate[loop_item],\n
isFullInventory[loop_item],\n
getSimulationState[loop_item]))">\n
</dtml-if>\n
</dtml-if>\n
</dtml-in>\n
\n
<dtml-if expr="len(row_list)">\n
REPLACE INTO\n
inventory\n
(`uid`, `node_uid`, `section_uid`, `payment_uid`, `date`, `is_full_inventory`, `simulation_state`)\n
VALUES\n
<dtml-in prefix="row" expr="row_list">\n
(<dtml-sqlvar expr="row_item[0]" type="int">,\n
<dtml-sqlvar expr="row_item[1]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[2]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[3]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[4]" type="datetime">,\n
<dtml-sqlvar expr="row_item[5] or 0" type="int">,\n
<dtml-sqlvar expr="row_item[6]" type="string">\n
)<dtml-if sequence-end><dtml-else>,</dtml-if>\n
</dtml-in>\n
</dtml-if>\n
</dtml-let>
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
erp5_base erp5_base
\ No newline at end of file erp5_stock_cache
571 573
\ No newline at end of file \ No newline at end of file
erp5_base erp5_base
\ No newline at end of file
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<value>1</value> <value>1</value>
</item> </item>
<item key="_filter_expression_archive" type="str"> <item key="_filter_expression_archive" type="str">
<value>python: context.providesIMovement() and context.isInventoryMovement()</value> <value>python: context.providesIMovement() and not context.isInventoryMovement()</value>
</item> </item>
<item key="_filter_expression_cache_key_archive" type="tuple"> <item key="_filter_expression_cache_key_archive" type="tuple">
<value>portal_type</value> <value>portal_type</value>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string>from Products.ERP5Type.Errors import ProgrammingError\n
\n
min_date = None\n
for loop_item in xrange(len(uid)):\n
if not isInventoryMovement[loop_item] and isMovement[loop_item] and getResourceUid[loop_item]:\n
if getDestinationUid[loop_item] and getStopDate[loop_item]:\n
if min_date:\n
min_date = min(min_date, getStopDate[loop_item])\n
else:\n
min_date = getStopDate[loop_item]\n
if getSourceUid[loop_item] and getStartDate[loop_item]:\n
if min_date:\n
min_date = min(min_date, getStartDate[loop_item])\n
else:\n
min_date = getStartDate[loop_item]\n
if min_date:\n
try:\n
context.SimulationTool_zTrimInventoryCacheFromDateOnCatalog(date=min_date)\n
except ProgrammingError:\n
# Create table if it does not exits\n
# Then no need to flush an empty table\n
context.SimulationTool_zCreateInventoryCache()\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>uid, isMovement, isInventoryMovement, getResourceUid, getDestinationUid, getStopDate, getSourceUid, getStartDate</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SQLCatalog_trimInventoryCacheOnCatalog</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string>from Products.ERP5Type.Errors import ProgrammingError\n
\n
try:\n
context.SimulationTool_zTrimInventoryCacheFromDateOnUncatalog(uid=uid)\n
except ProgrammingError:\n
# Create table if it does not exits\n
# Then no need to flush an empty table\n
context.SimulationTool_zCreateInventoryCache()\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>uid</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SQLCatalog_trimInventoryCacheOnUncatalog</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Folder" module="OFS.Folder"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_objects</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>erp5_stock_cache</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -6,14 +6,32 @@ ...@@ -6,14 +6,32 @@
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item>
<key> <string>allow_simple_one_argument_traversal</string> </key>
<value>
<none/>
</value>
</item>
<item> <item>
<key> <string>arguments_src</string> </key> <key> <string>arguments_src</string> </key>
<value> <string>column_value_list_list:list\r\n <value> <string>query\r\n
column_id_list:list\r\n date</string> </value>
group_by_expression\r\n </item>
date\r\n <item>
simulation_state=delivered\r\n <key> <string>cache_time_</string> </key>
where_expression</string> </value> <value> <int>0</int> </value>
</item>
<item>
<key> <string>class_file_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>class_name_</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_hook</string> </key>
<value> <string></string> </value>
</item> </item>
<item> <item>
<key> <string>connection_id</string> </key> <key> <string>connection_id</string> </key>
...@@ -21,39 +39,31 @@ where_expression</string> </value> ...@@ -21,39 +39,31 @@ where_expression</string> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>Resource_zGetFullInventoryDate</string> </value> <value> <string>Resource_zGetInventoryCacheResult</string> </value>
</item>
<item>
<key> <string>max_cache_</string> </key>
<value> <int>100</int> </value>
</item>
<item>
<key> <string>max_rows_</string> </key>
<value> <int>1</int> </value>
</item> </item>
<item> <item>
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[ <value> <string encoding="cdata"><![CDATA[
SELECT\n SELECT \n
MAX(date) as date,\n date, \n
<dtml-var group_by_expression>\n result \n
FROM\n FROM \n
inventory\n inventory_cache \n
WHERE\n WHERE \n
is_full_inventory = TRUE\n inventory_cache.query=<dtml-sqlvar query type="string"> \n
<dtml-if simulation_state>\n AND \n
AND simulation_state = <dtml-sqlvar simulation_state type="string">\n inventory_cache.date <= <dtml-sqlvar date type="datetime"> \n
</dtml-if>\n ORDER BY \n
<dtml-in prefix="loop" expr="_.range(_.len(column_id_list))">\n date DESC
AND <dtml-var expr="column_id_list[loop_item]"> IN (\n
<dtml-in expr="column_value_list_list[loop_item]">\n
<dtml-sqlvar sequence-item type="int">\n
<dtml-if sequence-end><dtml-else>,</dtml-if>\n
</dtml-in>\n
)\n
</dtml-in>\n
<dtml-if date>\n
AND date <= <dtml-sqlvar date type="string">\n
</dtml-if>\n
<dtml-if where_expression>\n
AND <dtml-var where_expression>\n
</dtml-if>\n
GROUP BY\n
<dtml-var group_by_expression>\n
]]></string> </value> ]]></string> </value>
</item> </item>
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>query\r\n
date\r\n
result</string> </value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Resource_zInsertInventoryCacheResult</string> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
Insert into inventory_cache(`query`, `date`, `result`) values (<dtml-sqlvar query type="string">, <dtml-sqlvar date type="datetime">, <dtml-sqlvar result type="string">)
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string># XXX To be implemented\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SimulationTool_flushInventoryCache</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_body</string> </key>
<value> <string>"""\n
Returns a duration, in days, for stock cache management.\n
If data in stock cache is older than lag compared to query\'s date\n
(at_date or to_date), then it becomes a "soft miss": use found value,\n
but add a new entry to cache at query\'s date minus half the lag.\n
So this value should be:\n
- Small enough that few enough rows need to be table-scanned for\n
verage queries (probably queries against current date).\n
- Large enough that few enough documents get modified past that date,\n
therwise cache entries would be removed from cache all the time.\n
"""\n
\n
return 60\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SimulationTool_getInventoryCacheLag</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -16,11 +16,17 @@ ...@@ -16,11 +16,17 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>z0_drop_inventory</string> </value> <value> <string>SimulationTool_zCreateInventoryCache</string> </value>
</item> </item>
<item> <item>
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string>DROP TABLE IF EXISTS inventory</string> </value> <value> <string>Create table `inventory_cache` (\n
`query` BINARY(16) NOT NULL,\n
`date` datetime NOT NULL,\n
`result` LONGBLOB NOT NULL,\n
PRIMARY KEY (`query`, `date`),\n
KEY (`date`)\n
) Engine=InnoDB</string> </value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
......
...@@ -6,6 +6,12 @@ ...@@ -6,6 +6,12 @@
</pickle> </pickle>
<pickle> <pickle>
<dictionary> <dictionary>
<item>
<key> <string>_col</string> </key>
<value>
<tuple/>
</value>
</item>
<item> <item>
<key> <string>arguments_src</string> </key> <key> <string>arguments_src</string> </key>
<value> <string></string> </value> <value> <string></string> </value>
...@@ -16,11 +22,11 @@ ...@@ -16,11 +22,11 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>z0_drop_inventory_stock</string> </value> <value> <string>SimulationTool_zDropInventoryCache</string> </value>
</item> </item>
<item> <item>
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string>DROP TABLE IF EXISTS inventory_stock</string> </value> <value> <string>DROP TABLE IF EXISTS inventory_cache</string> </value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>arguments_src</string> </key> <key> <string>arguments_src</string> </key>
<value> <string>uid</string> </value> <value> <string>date</string> </value>
</item> </item>
<item> <item>
<key> <string>connection_id</string> </key> <key> <string>connection_id</string> </key>
...@@ -16,13 +16,16 @@ ...@@ -16,13 +16,16 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>z0_uncatalog_inventory_stock</string> </value> <value> <string>SimulationTool_zTrimInventoryCacheFromDateOnCatalog</string> </value>
</item> </item>
<item> <item>
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[ <value> <string encoding="cdata"><![CDATA[
DELETE FROM inventory_stock WHERE <dtml-sqltest uid op=eq type=int> DELETE FROM \n
inventory_cache \n
WHERE \n
date > <dtml-sqlvar expr="date" type="datetime">
]]></string> </value> ]]></string> </value>
</item> </item>
......
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>z0_uncatalog_inventory</string> </value> <value> <string>SimulationTool_zTrimInventoryCacheFromDateOnUncatalog</string> </value>
</item> </item>
<item> <item>
<key> <string>src</string> </key> <key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[ <value> <string encoding="cdata"><![CDATA[
DELETE FROM inventory WHERE <dtml-sqltest uid op=eq type=int> DELETE FROM inventory_cache WHERE date > (SELECT min(date) from stock where <dtml-sqltest uid op=eq type=int>)
]]></string> </value> ]]></string> </value>
</item> </item>
......
2013-02-05 Aurel
Initial version
\ No newline at end of file
Copyright (c) 2001-2013 Nexedi SA
\ No newline at end of file
erp5_core
\ No newline at end of file
This bt5 contains code to activate the stock caching optimisation
\ No newline at end of file
GPL
\ No newline at end of file
aurel
\ No newline at end of file
4
\ No newline at end of file
erp5_mysql_innodb/SQLCatalog_trimInventoryCacheOnCatalog
erp5_mysql_innodb/SQLCatalog_trimInventoryCacheOnUncatalog
\ No newline at end of file
erp5_stock_cache
\ No newline at end of file
erp5_stock_cache
\ No newline at end of file
1.0
\ No newline at end of file
...@@ -74,7 +74,7 @@ class Inventory(Delivery): ...@@ -74,7 +74,7 @@ class Inventory(Delivery):
def appendToCategoryList(self, category_list, value, base_category): def appendToCategoryList(self, category_list, value, base_category):
category_list.append("%s/%s" %(base_category, value)) category_list.append("%s/%s" %(base_category, value))
def splitAndExtendToCategoryList(self, category_list, value, *args, **kw): def splitAndExtendToCategoryList(self, category_list, value, *args, **kw):
if value is not None: if value is not None:
value_list = value.split('\n') value_list = value.split('\n')
...@@ -101,16 +101,16 @@ class Inventory(Delivery): ...@@ -101,16 +101,16 @@ class Inventory(Delivery):
# with not all properties defined and thus making # with not all properties defined and thus making
# request with no condition in mysql # request with no condition in mysql
object_list = [self] object_list = [self]
immediate_reindex_archive = sql_catalog_id is not None immediate_reindex_archive = sql_catalog_id is not None
self.portal_catalog.catalogObjectList(object_list, self.portal_catalog.catalogObjectList(object_list,
sql_catalog_id = sql_catalog_id, sql_catalog_id = sql_catalog_id,
disable_archive=disable_archive, disable_archive=disable_archive,
immediate_reindex_archive=immediate_reindex_archive) immediate_reindex_archive=immediate_reindex_archive)
return return
connection_id = None connection_id = None
if sql_catalog_id is not None: if sql_catalog_id is not None:
# try to get connection used in the catalog # try to get connection used in the catalog
catalog = self.portal_catalog[sql_catalog_id] catalog = self.portal_catalog[sql_catalog_id]
for method in catalog.objectValues(): for method in catalog.objectValues():
if method.meta_type == "Z SQL Method": if method.meta_type == "Z SQL Method":
...@@ -139,10 +139,10 @@ class Inventory(Delivery): ...@@ -139,10 +139,10 @@ class Inventory(Delivery):
}, },
) )
method = self._getTypeBasedMethod('getDefaultInventoryCalculationList') method = self._getTypeBasedMethod('getDefaultInventoryCalculationList')
if method is not None: if method is not None:
default_inventory_calculation_list = method() default_inventory_calculation_list = method()
if temp_constructor is None: if temp_constructor is None:
from Products.ERP5Type.Document import newTempMovement from Products.ERP5Type.Document import newTempMovement
...@@ -165,13 +165,11 @@ class Inventory(Delivery): ...@@ -165,13 +165,11 @@ class Inventory(Delivery):
current_inventory_dict = {} current_inventory_dict = {}
current_inventory_key_id_list = [x["key"] for x in inventory_calculation_dict['first_level']] current_inventory_key_id_list = [x["key"] for x in inventory_calculation_dict['first_level']]
for line in current_inventory_list: for line in current_inventory_list:
current_inventory_key = [line[x] for x in current_inventory_key_id_list] current_inventory_key = [line[x] for x in current_inventory_key_id_list]
for x in xrange(len(current_inventory_key)): for x in xrange(len(current_inventory_key)):
if current_inventory_key[x] is None: if current_inventory_key[x] is None:
current_inventory_key[x] = "" current_inventory_key[x] = ""
current_inventory_key = tuple(current_inventory_key) current_inventory_key = tuple(current_inventory_key)
if inventory_calculation_dict.has_key("second_level"): if inventory_calculation_dict.has_key("second_level"):
# two level of variation # two level of variation
...@@ -189,10 +187,13 @@ class Inventory(Delivery): ...@@ -189,10 +187,13 @@ class Inventory(Delivery):
current_inventory_dict[current_inventory_key] = line['total_quantity'] current_inventory_dict[current_inventory_key] = line['total_quantity']
# Browse all movements on inventory and create diff line when necessary # Browse all movements on inventory and create diff line when necessary
not_used_inventory_dict = {} if self.isFullInventory():
not_used_inventory_dict = current_inventory_dict
else:
not_used_inventory_dict = {}
inventory_id = self.getId() inventory_id = self.getId()
list_method = inventory_calculation_dict['list_method'] list_method = inventory_calculation_dict['list_method']
method = getattr(self, list_method) method = getattr(self, list_method)
for movement in method(): for movement in method():
if movement.getResourceValue() is not None and \ if movement.getResourceValue() is not None and \
movement.getInventoriatedQuantity() not in (None, ''): movement.getInventoriatedQuantity() not in (None, ''):
...@@ -219,26 +220,26 @@ class Inventory(Delivery): ...@@ -219,26 +220,26 @@ class Inventory(Delivery):
second_key_list = tuple(second_key_list) second_key_list = tuple(second_key_list)
if inventory_value.has_key(second_key_list): if inventory_value.has_key(second_key_list):
total_quantity = inventory_value.pop(second_key_list) total_quantity = inventory_value.pop(second_key_list)
# Put remaining subvariation in a dict to know which one # Put remaining subvariation in a dict to know which one
# to removed at end # to removed at end
not_used_inventory_dict[tuple(key_list)] = inventory_value not_used_inventory_dict[tuple(key_list)] = inventory_value
diff_quantity = movement_quantity - total_quantity diff_quantity = movement_quantity - total_quantity
else: else:
# Inventory for new resource/variation/sub_variation # Inventory for new resource/variation/sub_variation
diff_quantity = movement_quantity diff_quantity = movement_quantity
# Put remaining subvariation in a dict to know which one # Put remaining subvariation in a dict to know which one
# to removed at end # to removed at end
not_used_inventory_dict[tuple(key_list)] = inventory_value not_used_inventory_dict[tuple(key_list)] = inventory_value
else: else:
# we got the quantity from first level key # we got the quantity from first level key
diff_quantity = movement_quantity - inventory_value diff_quantity = movement_quantity - inventory_value
# Create tmp movement # Create tmp movement
kwd = {'uid': movement.getUid(), kwd = {'uid': movement.getUid(),
'start_date': stop_date} 'start_date': stop_date}
temp_delivery_line = temp_constructor(self, temp_delivery_line = temp_constructor(self,
inventory_id) inventory_id)
# set category on it only if quantity not null # set category on it only if quantity not null
# thus line with same uid will be deleted but we # thus line with same uid will be deleted but we
# don't insert line with null quantity as we test # don't insert line with null quantity as we test
...@@ -246,7 +247,7 @@ class Inventory(Delivery): ...@@ -246,7 +247,7 @@ class Inventory(Delivery):
# before insert but not before delete # before insert but not before delete
if diff_quantity != 0: if diff_quantity != 0:
kwd['quantity'] = diff_quantity kwd['quantity'] = diff_quantity
category_list = self.getCategoryList() category_list = self.getCategoryList()
setter_list = [x['setter'] for x in inventory_calculation_dict['first_level']] setter_list = [x['setter'] for x in inventory_calculation_dict['first_level']]
if inventory_calculation_dict.has_key("second_level"): if inventory_calculation_dict.has_key("second_level"):
...@@ -267,13 +268,14 @@ class Inventory(Delivery): ...@@ -267,13 +268,14 @@ class Inventory(Delivery):
temp_delivery_line.edit(**kwd) temp_delivery_line.edit(**kwd)
stock_append(temp_delivery_line) stock_append(temp_delivery_line)
# Now create line to remove some subvariation text not present # Now create line to remove some subvariation text not present
# in new inventory # in new inventory
if len(not_used_inventory_dict): if len(not_used_inventory_dict):
inventory_uid = self.getUid() inventory_uid = self.getUid()
for first_level_key in not_used_inventory_dict.keys(): for first_level_key in not_used_inventory_dict.keys():
inventory_value = \ inventory_value = \
not_used_inventory_dict[tuple(first_level_key)] not_used_inventory_dict[tuple(first_level_key)]
# XXX-Aurel : this code does not work with only one level of variation
for second_level_key in inventory_value.keys(): for second_level_key in inventory_value.keys():
diff_quantity = - inventory_value[tuple(second_level_key)] diff_quantity = - inventory_value[tuple(second_level_key)]
...@@ -284,7 +286,7 @@ class Inventory(Delivery): ...@@ -284,7 +286,7 @@ class Inventory(Delivery):
temp_delivery_line = temp_constructor(self, temp_delivery_line = temp_constructor(self,
inventory_id) inventory_id)
kwd['quantity'] = diff_quantity kwd['quantity'] = diff_quantity
category_list = self.getCategoryList() category_list = self.getCategoryList()
setter_list = [x['setter'] for x in inventory_calculation_dict['first_level']] setter_list = [x['setter'] for x in inventory_calculation_dict['first_level']]
if inventory_calculation_dict.has_key("second_level"): if inventory_calculation_dict.has_key("second_level"):
...@@ -307,7 +309,7 @@ class Inventory(Delivery): ...@@ -307,7 +309,7 @@ class Inventory(Delivery):
# Reindex objects # Reindex objects
object_list = [self] object_list = [self]
immediate_reindex_archive = sql_catalog_id is not None immediate_reindex_archive = sql_catalog_id is not None
self.portal_catalog.catalogObjectList(object_list, self.portal_catalog.catalogObjectList(object_list,
sql_catalog_id = sql_catalog_id, sql_catalog_id = sql_catalog_id,
disable_archive=disable_archive, disable_archive=disable_archive,
......
...@@ -36,7 +36,7 @@ from Products.ERP5Type.Tool.BaseTool import BaseTool ...@@ -36,7 +36,7 @@ from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5 import _dtmldir from Products.ERP5 import _dtmldir
from zLOG import LOG, PROBLEM from zLOG import LOG, PROBLEM, WARNING, INFO
from Products.ERP5.Capacity.GLPK import solve from Products.ERP5.Capacity.GLPK import solve
from numpy import zeros, resize from numpy import zeros, resize
...@@ -50,8 +50,19 @@ from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery ...@@ -50,8 +50,19 @@ from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
from Shared.DC.ZRDB.Results import Results from Shared.DC.ZRDB.Results import Results
from Products.ERP5Type.Utils import mergeZRDBResults from Products.ERP5Type.Utils import mergeZRDBResults
from App.Extensions import getBrain
from MySQLdb import ProgrammingError
from hashlib import md5
from warnings import warn from warnings import warn
from cPickle import loads, dumps
from copy import deepcopy
from sys import exc_info
MYSQL_MIN_DATETIME_RESOLUTION = 1/86400.
class StockOptimisationError(Exception):
pass
class SimulationTool(BaseTool): class SimulationTool(BaseTool):
""" """
...@@ -1278,318 +1289,338 @@ class SimulationTool(BaseTool): ...@@ -1278,318 +1289,338 @@ class SimulationTool(BaseTool):
sql_source_list = [] sql_source_list = []
# If no group at all, give a default sort group by # If no group at all, give a default sort group by
kw.update(self._getDefaultGroupByParameters(**kw)) kw.update(self._getDefaultGroupByParameters(**kw))
base_inventory_kw = {
'stock_table_id': default_stock_table,
'src__': src__,
'ignore_variation': ignore_variation,
'standardise': standardise,
'omit_simulation': omit_simulation,
'only_accountable': only_accountable,
'selection_domain': selection_domain,
'selection_report': selection_report,
'precision': precision,
'inventory_list': inventory_list,
'connection_id': connection_id,
'statistic': statistic,
'convert_quantity_result': convert_quantity_result,
'quantity_unit_uid': quantity_unit_uid,
}
# Get cached data
if getattr(self, "Resource_zGetInventoryCacheResult", None) is not None and \
optimisation__ and 'from_date' not in kw and \
(('at_date' in kw) ^ ('to_date' in kw)) and \
'transformed_resource' not in kw:
# Here is the different kind of date
# from_date : >=
# to_date : <
# at_date : <=
# As we just have from_date, it means that we must use
# the to_date for the cache in order to avoid double computation
# of the same line
at_date = kw.pop("at_date", None)
if at_date is None:
to_date = kw.pop("to_date")
else:
# add one second so that we can use to_date
to_date = at_date + MYSQL_MIN_DATETIME_RESOLUTION
try:
cached_result, cached_date = self._getCachedInventoryList(
to_date=to_date,
sql_kw=kw,
**base_inventory_kw)
except StockOptimisationError:
cached_result = []
kw['to_date'] = to_date
else:
if src__:
sql_source_list.extend(cached_result)
# Now must generate query for date diff
kw['to_date'] = to_date
kw['from_date'] = cached_date
else:
cached_result = []
sql_kw, new_kw = self._generateKeywordDict(**kw) sql_kw, new_kw = self._generateKeywordDict(**kw)
# Copy kw content as _generateSQLKeywordDictFromKeywordDict
# remove some values from it
try:
new_kw_copy = deepcopy(new_kw)
except TypeError:
# new_kw contains wrong parameters
# as optimisation has already been disable we
# do not care about the deepcopy
new_kw_copy = new_kw
stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict( stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict(
table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw) table=default_stock_table, sql_kw=sql_kw, new_kw=new_kw_copy)
Resource_zGetFullInventoryDate = \ stock_sql_kw.update(base_inventory_kw)
getattr(self, 'Resource_zGetFullInventoryDate', None) delta_result = self.Resource_zGetInventoryList(
EQUAL_DATE_TABLE_ID = 'inventory_stock' **stock_sql_kw)
GREATER_THAN_DATE_TABLE_ID = 'stock'
buildSQLQuery = self.getPortalObject().portal_catalog.buildSQLQuery
optimisation_success = optimisation__ and ('from_date' not in kw) and \
Resource_zGetFullInventoryDate is not None and \
(GREATER_THAN_DATE_TABLE_ID == default_stock_table)
# Generate first query parameter dict
if optimisation_success:
def getFirstQueryParameterDict(query_generator_kw):
optimisation_success = True
AVAILABLE_CRITERIONS_IN_INVENTORY_TABLE = ['node_uid',
'section_uid',
'payment_uid']
# Only column group by are supported in full inventories
group_by_list = query_generator_kw.get('column_group_by', [])
column_value_dict = query_generator_kw.get('column_value_dict', {})
new_group_by_list = []
new_column_value_dict = {}
for criterion_id in AVAILABLE_CRITERIONS_IN_INVENTORY_TABLE:
criterion_value_list = column_value_dict.get(criterion_id, [])
if not isinstance(criterion_value_list, (list, tuple)):
criterion_value_list = [criterion_value_list]
if len(criterion_value_list) > 0:
if len(criterion_value_list) > 1:
# Impossible to optimise if there is more than one possible
# value per criterion.
optimisation_success = False
break
new_column_value_dict[criterion_id] = criterion_value_list
new_group_by_list.append(criterion_id)
elif criterion_id in group_by_list:
new_group_by_list.append(criterion_id)
group_by_expression = ', '.join(new_group_by_list)
column_id_list = new_column_value_dict.keys()
column_value_list_list = new_column_value_dict.values()
date_value_list = column_value_dict.get('date', {}).get('query', [])
where_expression = None
if len(date_value_list) == 1:
date = date_value_list[0]
# build a query for date to take range into account
date_query_result = buildSQLQuery(**{
'inventory.date': {
'query': date,
'range': column_value_dict.get('date', {}).get('range', [])
},
'query_table': None,
})
if date_query_result['where_expression'] not in ('',None):
where_expression = date_query_result['where_expression']
elif len(date_value_list) > 1:
# When more than one date is provided, we must not optimise.
# Also, as we should never end up here (the only currently known
# case where there are 2 dates is when a from_date is provided
# along with either an at_date or a to_date, and we disable
# optimisation when from_date is given), emit a log.
# This can happen if there are more date parameters than mentioned
# above.
LOG('SimulationTool', PROBLEM, 'There is more than one date condition'
' so optimisation got disabled. The result of this call will be'
' correct but it requires investigation as some cases might'
' have gone unnoticed and produced wrong results.')
optimisation_success = False
return {'group_by_expression': group_by_expression,
'column_id_list': column_id_list,
'column_value_list_list': column_value_list_list,
'where_expression' : where_expression,}, optimisation_success
first_query_param_dict, optimisation_success = getFirstQueryParameterDict(new_kw)
if optimisation_success:
if len(first_query_param_dict['column_id_list']):
inventory_date_line_list = self.Resource_zGetFullInventoryDate(
**first_query_param_dict)
if src__:
sql_source_list.append(
self.Resource_zGetFullInventoryDate(src__=src__,
**first_query_param_dict))
# Check that all expected uids have been found, otherwise a full
# inventory of a node/section/payment might be missing.
if len(inventory_date_line_list) >= max([len(x) for x in \
first_query_param_dict['column_value_list_list']]):
# Generate a where expression which filters on dates retrieved
# in the first query to be used in the second query.
# Also, generate a where expression to use in the third query,
# since it is based on the same data.
# XXX: uggly duplicated query generation code
# XXX: duplicates SQL variable formatting present in
# ERP5Type/patches/sqlvar.py about datetime SQL columns.
# Note: This code can generate queries like:
# date = 2000/01/01 and date >= 2001/01/01
# When latest full inventory is at 2000/01/01 and given
# from_date is 2001/01/01.
# It is not a serious problem since MySQL detects incompatible
# conditions and immediately returns (with 0 rows).
# get search key definitions from portal_catalog
ctool = getToolByName(self, 'portal_catalog')
portal_catalog = ctool.getSQLCatalog()
keyword_search_keys = list(portal_catalog.sql_catalog_keyword_search_keys)
datetime_search_keys = list(portal_catalog.sql_catalog_datetime_search_keys)
full_text_search_keys = list(portal_catalog.sql_catalog_full_text_search_keys)
search_key_mapping = dict(key_alias_dict = None,
keyword_search_keys = keyword_search_keys,
datetime_search_keys = datetime_search_keys,
full_text_search_keys = full_text_search_keys)
equal_date_query_list = []
greater_than_date_query_list = []
for inventory_date_line_dict in \
inventory_date_line_list.dictionaries():
date = inventory_date_line_dict['date']
non_date_value_dict = dict([(k, v) for k, v \
in inventory_date_line_dict.iteritems() if k != 'date'])
equal_date_query_list.append(
ComplexQuery(
ComplexQuery(operator='AND',
*[Query(**{'%s.%s' % (EQUAL_DATE_TABLE_ID, k): v}) \
for k, v in non_date_value_dict.iteritems()]),
Query(**{'%s.date' % (EQUAL_DATE_TABLE_ID, ): date}),
operator='AND'))
greater_than_date_query_list.append(
ComplexQuery(
ComplexQuery(operator='AND',
*[Query(**{'%s.%s' % (GREATER_THAN_DATE_TABLE_ID, k): \
v}) \
for k, v in non_date_value_dict.iteritems()]),
# 'Use explicitly Universal' otherwise DateTime
# search key will convert it to UTC one more time
Query(**{'%s.date' % (GREATER_THAN_DATE_TABLE_ID, ): date,
'range': 'nlt'}),
operator='AND'))
assert len(equal_date_query_list) == \
len(greater_than_date_query_list)
assert len(equal_date_query_list) > 0
equal_date_query = buildSQLQuery(query=ComplexQuery(operator='OR', *equal_date_query_list), query_table=None)['where_expression']
greater_than_date_query = buildSQLQuery(query=ComplexQuery(operator='OR', *greater_than_date_query_list), query_table=None)['where_expression']
inventory_stock_sql_kw = \
self._generateSQLKeywordDictFromKeywordDict(
table=EQUAL_DATE_TABLE_ID, sql_kw=sql_kw, new_kw=new_kw)
inventory_stock_where_query = \
inventory_stock_sql_kw.get('where_expression', '(1)')
assert isinstance(inventory_stock_where_query, basestring) \
and len(inventory_stock_where_query)
inventory_stock_sql_kw['where_expression'] = '(%s) AND (%s)' % \
(inventory_stock_where_query, equal_date_query)
where_query = stock_sql_kw.get('where_expression', '(1)')
assert isinstance(where_query, basestring) and len(where_query)
stock_sql_kw['where_expression'] = '(%s) AND (%s)' % \
(where_query, greater_than_date_query)
# Get initial inventory amount
initial_inventory_line_list = self.Resource_zGetInventoryList(
stock_table_id=EQUAL_DATE_TABLE_ID,
src__=src__, ignore_variation=ignore_variation,
standardise=standardise, omit_simulation=omit_simulation,
only_accountable=only_accountable,
selection_domain=selection_domain,
selection_report=selection_report, precision=precision,
inventory_list=inventory_list,
statistic=statistic,
quantity_unit_uid=quantity_unit_uid,
convert_quantity_result=convert_quantity_result,
**inventory_stock_sql_kw)
# Get delta inventory
delta_inventory_line_list = self.Resource_zGetInventoryList(
stock_table_id=GREATER_THAN_DATE_TABLE_ID,
src__=src__, ignore_variation=ignore_variation,
standardise=standardise, omit_simulation=omit_simulation,
only_accountable=only_accountable,
selection_domain=selection_domain,
selection_report=selection_report, precision=precision,
inventory_list=inventory_list,
statistic=statistic,
quantity_unit_uid=quantity_unit_uid,
convert_quantity_result=convert_quantity_result,
**stock_sql_kw)
# Match & add initial and delta inventories
if src__:
sql_source_list.extend((initial_inventory_line_list,
delta_inventory_line_list))
else:
if 'column_group_by' in new_kw:
group_by_id_list = []
group_by_id_list_append = group_by_id_list.append
for group_by_id in new_kw['column_group_by']:
if group_by_id == 'uid':
group_by_id_list_append('stock_uid')
else:
group_by_id_list_append(group_by_id)
def getInventoryListKey(line):
"""
Generate a key based on values used in SQL group_by
"""
return tuple([line[x] for x in group_by_id_list])
else:
def getInventoryListKey(line):
"""
No group by criterion, regroup everything.
"""
return 'dummy_key'
result_column_id_dict['inventory'] = None
result_column_id_dict['total_quantity'] = None
result_column_id_dict['total_price'] = None
def addLineValues(line_a=None, line_b=None):
"""
Addition columns of 2 lines and return a line with same
schema. If one of the parameters is None, returns the
other parameters.
Arithmetic modifications on additions:
None + x = x
None + None = None
"""
if line_a is None:
return line_b
if line_b is None:
return line_a
# Create a new Shared.DC.ZRDB.Results.Results.__class__
# instance to add the line values.
# the logic for the 5 lines below is taken from
# Shared.DC.ZRDB.Results.Results.__getitem__
Result = line_a.__class__
parent = line_a.aq_parent
result = Result((), parent)
if parent is not None:
result = result.__of__(parent)
for key in line_a.__record_schema__:
value = line_a[key]
if key in result_column_id_dict:
value_b = line_b[key]
if None not in (value, value_b):
result[key] = value + value_b
elif value is not None:
result[key] = value
else:
result[key] = value_b
elif line_a[key] == line_b[key]:
result[key] = line_a[key]
elif key not in ('date', 'stock_uid', 'path'):
LOG('InventoryTool.getInventoryList.addLineValues',
PROBLEM,
'mismatch for %s column: %s and %s' % \
(key, line_a[key], line_b[key]))
return result
inventory_list_dict = {}
for line_list in (initial_inventory_line_list,
delta_inventory_line_list):
for line in line_list:
line_key = getInventoryListKey(line)
line_a = inventory_list_dict.get(line_key)
inventory_list_dict[line_key] = addLineValues(line_a,
line)
## XXX: Returns a dict instead of an <r> instance
## As long as they are accessed like dicts it's ok, but...
#result = inventory_list_dict.values()
sorted_inventory_list = inventory_list_dict.values()
sort_on = new_kw.get('sort_on', tuple())
if len(sort_on) != 0:
def cmp_inventory_line(line_a, line_b):
"""
Compare 2 inventory lines and sort them according to
sort_on parameter.
"""
result = 0
for key, sort_direction in sort_on:
if not(key in line_a and key in line_b):
raise Exception, "Impossible to sort result since " \
"columns sort happens on are not available in " \
"result."
result = cmp(line_a[key], line_b[key])
if result != 0:
if len(sort_direction[0]) and \
sort_direction[0].upper() != 'A':
# Default sort is ascending, if a sort is given and
# it does not start with an 'A' then reverse sort.
# Tedious syntax checking is MySQL's job, and
# happened when queries were executed.
result *= -1
break
return result
sorted_inventory_list.sort(cmp_inventory_line)
result = Results((delta_inventory_line_list.\
_searchable_result_columns(),
tuple(sorted_inventory_list)))
else:
# Not all required full inventories are found
optimisation_success = False
else:
# Not enough criterions to trigger optimisation
optimisation_success = False
if not optimisation_success:
result = self.Resource_zGetInventoryList(
stock_table_id=default_stock_table,
src__=src__, ignore_variation=ignore_variation,
standardise=standardise, omit_simulation=omit_simulation,
only_accountable=only_accountable,
selection_domain=selection_domain,
selection_report=selection_report, precision=precision,
inventory_list=inventory_list, connection_id=connection_id,
statistic=statistic,
quantity_unit_uid=quantity_unit_uid,
convert_quantity_result=convert_quantity_result,
**stock_sql_kw)
if src__:
sql_source_list.append(result)
if src__: if src__:
sql_source_list.append(delta_result)
result = ';\n-- NEXT QUERY\n'.join(sql_source_list) result = ';\n-- NEXT QUERY\n'.join(sql_source_list)
else:
if cached_result:
result = self._addBrainResults(delta_result, cached_result, new_kw)
else:
result = delta_result
return result return result
def getInventoryCacheLag(self):
"""
Returns a duration, in days, for stock cache management.
If data in stock cache is older than lag compared to query's date
(at_date or to_date), then it becomes a "soft miss": use found value,
but add a new entry to cache at query's date minus half the lag.
So this value should be:
- Small enough that few enough rows need to be table-scanned for
average queries (probably queries against current date).
- Large enough that few enough documents get modified past that date,
otherwise cache entries would be removed from cache all the time.
"""
return self.SimulationTool_getInventoryCacheLag()
def _getCachedInventoryList(self, to_date, sql_kw, stock_table_id, src__=False, **kw):
"""
Try to get a cached inventory list result
If not existing, fill the cache
"""
Resource_zGetInventoryList = self.Resource_zGetInventoryList
# Generate the SQL source without date parameter
# This will be the cache key
try:
no_date_kw = deepcopy(sql_kw)
except TypeError:
LOG("SimulationTool._getCachedInventoryList", WARNING,
"Failed copying sql_kw, disabling stock cache",
error=exc_info())
raise StockOptimisationError
no_date_sql_kw, no_date_new_kw = self._generateKeywordDict(**no_date_kw)
no_date_stock_sql_kw = self._generateSQLKeywordDictFromKeywordDict(
table=stock_table_id, sql_kw=no_date_sql_kw,
new_kw=no_date_new_kw)
kw.update(no_date_stock_sql_kw)
if src__:
sql_source_list = []
# Generate the cache key (md5 of query source)
sql_text_hash = md5(Resource_zGetInventoryList(
stock_table_id=stock_table_id,
src__=1,
**kw)).digest()
# Try to get result from cache
Resource_zGetInventoryCacheResult = self.Resource_zGetInventoryCacheResult
inventory_cache_kw = {
'query': sql_text_hash,
'date': to_date,
}
try:
cached_sql_result = Resource_zGetInventoryCacheResult(**inventory_cache_kw)
except ProgrammingError:
# First use of the optimisation, we need to create the table
LOG("SimulationTool._getCachedInventoryList", INFO,
"Creating inventory cache stock")
if src__:
sql_source_list.append(self.SimulationTool_zCreateInventoryCache(src__=1))
else:
self.SimulationTool_zCreateInventoryCache()
cached_sql_result = None
if src__:
sql_source_list.append(Resource_zGetInventoryCacheResult(src__=1, **inventory_cache_kw))
if cached_sql_result:
brain_result = loads(cached_sql_result[0].result)
# Rebuild the brains
cached_result = Results(
(brain_result['items'], brain_result['data']),
brains=getBrain(
Resource_zGetInventoryList.class_file_,
Resource_zGetInventoryList.class_name_,
),
parent=self,
)
else:
cached_result = []
cache_lag = self.getInventoryCacheLag()
if cached_sql_result and to_date - DateTime(cached_sql_result[0].date) < cache_lag:
cached_date = DateTime(cached_sql_result[0].date)
result = cached_result
else:
# Cache miss, or hit with old data: store a new entry in cache.
# Don't store it at to_date, as it risks being flushed soon (ie, when
# any document older than to_date gets reindexed in stock table).
# Don't store it at to_date - cache_lag, as it would risk expiring
# soon as we store it (except if to_date is fixed for many queries,
# which we cannot tell here).
# So store it at half the cache_lag before to_date.
cached_date = to_date - cache_lag / 2
new_cache_kw = deepcopy(sql_kw)
if cached_result:
# We can use cached result to generate new cache result
new_cache_kw['from_date'] = DateTime(cached_sql_result[0].date)
sql_kw, new_kw = self._generateKeywordDict(
to_date=cached_date,
**new_cache_kw)
kw.update(self._generateSQLKeywordDictFromKeywordDict(
table=stock_table_id,
sql_kw=sql_kw,
new_kw=new_kw,
)
)
new_result = Resource_zGetInventoryList(
stock_table_id=stock_table_id,
src__=src__,
**kw)
if src__:
sql_source_list.append(new_result)
else:
result = self._addBrainResults(new_result, cached_result, new_kw)
self.Resource_zInsertInventoryCacheResult(
query=sql_text_hash,
date=cached_date,
result=dumps({
'items': result.__items__,
'data': result._data,
}),
)
if src__:
result = sql_source_list
return result, cached_date
def _addBrainResults(self, first_result, second_result, new_kw):
"""
Build a Results which is the addition of two other result
"""
# This part defined key to group lines from different Results
group_by_id_list = []
group_by_id_list_append = group_by_id_list.append
for group_by_id in new_kw.get('column_group_by', []):
if group_by_id == 'uid':
group_by_id_list_append('stock_uid')
else:
group_by_id_list_append(group_by_id)
# Add related key group by
if 'select_list' in new_kw.get("related_key_dict_passthrough", []):
for group_by_id in new_kw["related_key_dict_passthrough"]['group_by']:
if group_by_id in new_kw["related_key_dict_passthrough"]["select_list"]:
group_by_id_list_append(group_by_id)
else:
# XXX-Aurel : to review & change, must prevent coming here before
raise ValueError, "Impossible to group by %s" %(group_by_id)
elif "group_by" in new_kw.get("related_key_dict_passthrough", []):
raise ValueError, "Impossible to group by %s" %(new_kw["related_key_dict_passthrough"]['group_by'],)
if len(group_by_id_list):
def getInventoryListKey(line):
"""
Generate a key based on values used in SQL group_by
"""
return tuple([line[x] for x in group_by_id_list])
else:
def getInventoryListKey(line):
"""
Return a dummy key, all line will be summed
"""
return "dummy"
result_column_id_dict = {
'inventory': None,
'total_quantity': None,
'total_price': None
}
def addLineValues(line_a=None, line_b=None):
"""
Add columns of 2 lines and return a line with same
schema. If one of the parameters is None, returns the
other parameters.
Arithmetic modifications on additions:
None + x = x
None + None = None
"""
if line_a is None:
return line_b
if line_b is None:
return line_a
# Create a new Shared.DC.ZRDB.Results.Results.__class__
# instance to add the line values.
# the logic for the 5 lines below is taken from
# Shared.DC.ZRDB.Results.Results.__getitem__
Result = line_a.__class__
parent = line_a.aq_parent
result = Result((), parent)
try:
# We must copy the path so that getObject works
setattr(result, 'path', line_a.path)
except ValueError: # XXX: ValueError ? really ?
# getInventory return no object, so no path available
pass
if parent is not None:
result = result.__of__(parent)
for key in line_a.__record_schema__:
value = line_a[key]
if key in result_column_id_dict:
value_b = line_b[key]
if None not in (value, value_b):
result[key] = value + value_b
elif value is not None:
result[key] = value
else:
result[key] = value_b
elif line_a[key] == line_b[key]:
result[key] = line_a[key]
elif key not in ('date', 'stock_uid', 'path'):
LOG('InventoryTool.getInventoryList.addLineValues',
PROBLEM,
'mismatch for %s column: %s and %s' % (
key, line_a[key], line_b[key]))
return result
# Add lines
inventory_list_dict = {}
for line_list in (first_result, second_result):
for line in line_list:
line_key = getInventoryListKey(line)
line_a = inventory_list_dict.get(line_key)
inventory_list_dict[line_key] = addLineValues(line_a, line)
sorted_inventory_list = inventory_list_dict.values()
# Sort results manually when required
sort_on = new_kw.get('sort_on')
if sort_on:
def cmp_inventory_line(line_a, line_b):
"""
Compare 2 inventory lines and sort them according to
sort_on parameter.
"""
result = 0
for key, sort_direction in sort_on:
try:
result = cmp(line_a[key], line_b[key])
except KeyError:
raise Exception('Impossible to sort result since columns sort '
'happens on are not available in result: %r' % (key, ))
if result:
if not sort_direction.upper().startswith('A'):
# Default sort is ascending, if a sort is given and
# it does not start with an 'A' then reverse sort.
# Tedious syntax checking is MySQL's job, and
# happened when queries were executed.
result *= -1
break
return result
sorted_inventory_list.sort(cmp_inventory_line)
# Brain is rebuild properly using tuple not r instance
column_list = first_result._searchable_result_columns()
column_name_list = [x['name'] for x in column_list]
# Rebuild a result object based on added results
Resource_zGetInventoryList = self.Resource_zGetInventoryList
return Results(
(column_list, tuple([tuple([getattr(y, x) for x in column_name_list]) \
for y in sorted_inventory_list])),
parent=self,
brains=getBrain(
Resource_zGetInventoryList.class_file_,
Resource_zGetInventoryList.class_name_,
),
)
security.declareProtected(Permissions.AccessContentsInformation, security.declareProtected(Permissions.AccessContentsInformation,
'getConvertedInventoryList') 'getConvertedInventoryList')
def getConvertedInventoryList(self, simulation_period='', **kw): def getConvertedInventoryList(self, simulation_period='', **kw):
......
...@@ -15,9 +15,6 @@ ...@@ -15,9 +15,6 @@
<key>effective_date</key> <key>effective_date</key>
<key>expiration_date</key> <key>expiration_date</key>
<key>grouping_date</key> <key>grouping_date</key>
<key>inventory.date</key>
<key>inventory_stock.date</key>
<key>inventory_stock.mirror_date</key>
<key>item.date</key> <key>item.date</key>
<key>mirror_date</key> <key>mirror_date</key>
<key>modification_date</key> <key>modification_date</key>
...@@ -34,4 +31,4 @@ ...@@ -34,4 +31,4 @@
<key>stop_date_range_min</key> <key>stop_date_range_min</key>
<key>versioning.effective_date</key> <key>versioning.effective_date</key>
<key>versioning.expiration_date</key> <key>versioning.expiration_date</key>
</key_list> </key_list>
\ No newline at end of file
<catalog_method>
<item key="sql_uncatalog_object" type="int">
<value>1</value>
</item>
<item key="_is_filtered_archive" type="int">
<value>1</value>
</item>
<item key="_filter_expression_archive" type="str">
<value>python: context.isInventory()</value>
</item>
<item key="_filter_expression_cache_key_archive" type="tuple">
<value>portal_type</value>
</item>
</catalog_method>
<catalog_method>
<item key="sql_catalog_object_list" type="int">
<value>1</value>
</item>
</catalog_method>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>isInventory\r\n
uid\r\n
getDestinationUid\r\n
getDestinationSectionUid\r\n
getDestinationPaymentUid\r\n
getStartDate\r\n
isFullInventory\r\n
getSimulationState</string> </value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_catalog_inventory_list</string> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
<dtml-let row_list="[]">\n
\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
<dtml-if expr="isInventory[loop_item]">\n
<dtml-if expr="getStartDate[loop_item] is not None">\n
<dtml-call expr="row_list.append((\n
uid[loop_item],\n
getDestinationUid[loop_item],\n
getDestinationSectionUid[loop_item],\n
getDestinationPaymentUid[loop_item],\n
getStartDate[loop_item],\n
isFullInventory[loop_item],\n
getSimulationState[loop_item]))">\n
</dtml-if>\n
</dtml-if>\n
</dtml-in>\n
\n
<dtml-if expr="len(row_list)">\n
REPLACE INTO\n
inventory\n
(`uid`, `node_uid`, `section_uid`, `payment_uid`, `date`, `is_full_inventory`, `simulation_state`)\n
VALUES\n
<dtml-in prefix="row" expr="row_list">\n
(<dtml-sqlvar expr="row_item[0]" type="int">,\n
<dtml-sqlvar expr="row_item[1]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[2]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[3]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[4]" type="datetime">,\n
<dtml-sqlvar expr="row_item[5] or 0" type="int">,\n
<dtml-sqlvar expr="row_item[6]" type="string">\n
)<dtml-if sequence-end><dtml-else>,</dtml-if>\n
</dtml-in>\n
</dtml-if>\n
</dtml-let>
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>arguments_src</string> </key>
<value> <string>uid\r\n
getResourceUid\r\n
getInventoriatedQuantity\r\n
getSourceUid\r\n
getDestinationUid\r\n
getSourceSectionUid\r\n
getDestinationSectionUid\r\n
isMovement\r\n
getSourcePaymentUid\r\n
getDestinationPaymentUid\r\n
getSourceFunctionUid\r\n
getDestinationFunctionUid\r\n
getSourceProjectUid\r\n
getDestinationProjectUid\r\n
getSourceFundingUid\r\n
getDestinationFundingUid\r\n
getSourcePaymentRequestUid\r\n
getDestinationPaymentRequestUid\r\n
getSimulationState\r\n
getSourceInventoriatedTotalAssetPrice\r\n
getDestinationInventoriatedTotalAssetPrice\r\n
getStartDate\r\n
getStopDate\r\n
isAccountable\r\n
getPortalType\r\n
getVariationText\r\n
getSubVariationText</string> </value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_catalog_inventory_stock_list</string> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string encoding="cdata"><![CDATA[
DELETE FROM\n
inventory_stock\n
WHERE\n
<dtml-in uid>\n
uid=<dtml-sqlvar sequence-item type="int"><dtml-if sequence-end><dtml-else> OR </dtml-if>\n
</dtml-in>\n
\n
<dtml-var sql_delimiter>\n
\n
<dtml-let row_list="[]" uid_dict="{}">\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
<dtml-if "isMovement[loop_item] and isAccountable[loop_item] and getResourceUid[loop_item]">\n
<dtml-if "getDestinationUid[loop_item]">\n
<dtml-call expr="uid_dict.update({uid[loop_item]: uid_dict.get(uid[loop_item], -1) + 1})">\n
<dtml-call expr="row_list.append([\n
uid[loop_item], \n
uid_dict[uid[loop_item]],\n
getDestinationUid[loop_item],\n
getDestinationSectionUid[loop_item],\n
getDestinationPaymentUid[loop_item],\n
getDestinationFunctionUid[loop_item],\n
getDestinationProjectUid[loop_item], \n
getDestinationFundingUid[loop_item], \n
getDestinationPaymentRequestUid[loop_item], \n
getSourceSectionUid[loop_item], \n
getSourceUid[loop_item], \n
getResourceUid[loop_item],\n
getInventoriatedQuantity[loop_item],\n
getStopDate[loop_item], \n
getStartDate[loop_item], \n
getDestinationInventoriatedTotalAssetPrice[loop_item], \n
getPortalType[loop_item], \n
getSimulationState[loop_item], \n
getVariationText[loop_item],\n
getSubVariationText[loop_item]])">\n
</dtml-if>\n
<dtml-if "getSourceUid[loop_item]">\n
<dtml-call expr="uid_dict.update({uid[loop_item]: uid_dict.get(uid[loop_item], -1) + 1})">\n
<dtml-call expr="row_list.append([\n
uid[loop_item], \n
uid_dict[uid[loop_item]],\n
getSourceUid[loop_item],\n
getSourceSectionUid[loop_item],\n
getSourcePaymentUid[loop_item],\n
getSourceFunctionUid[loop_item],\n
getSourceProjectUid[loop_item], \n
getSourceFundingUid[loop_item], \n
getSourcePaymentRequestUid[loop_item], \n
getDestinationSectionUid[loop_item], \n
getDestinationUid[loop_item], \n
getResourceUid[loop_item],\n
-(getInventoriatedQuantity[loop_item] or 0), \n
getStartDate[loop_item], \n
getStopDate[loop_item],\n
getSourceInventoriatedTotalAssetPrice[loop_item], \n
getPortalType[loop_item], \n
getSimulationState[loop_item], \n
getVariationText[loop_item],\n
getSubVariationText[loop_item]])">\n
</dtml-if>\n
</dtml-if>\n
</dtml-in> \n
\n
<dtml-if "row_list">\n
INSERT INTO\n
inventory_stock\n
VALUES\n
<dtml-in prefix="row" expr="row_list">\n
(\n
<dtml-sqlvar expr="row_item[0]" type="int">,\n
<dtml-sqlvar expr="row_item[1]" type="int">,\n
<dtml-sqlvar expr="row_item[2]" type="int">,\n
<dtml-sqlvar expr="row_item[3]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[4]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[5]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[6]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[7]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[8]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[9]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[10]" type="int" optional>,\n
<dtml-sqlvar expr="row_item[11]" type="int">,\n
<dtml-sqlvar expr="row_item[12]" type="float" optional>,\n
1, <dtml-comment>only accountable</dtml-comment>\n
<dtml-sqlvar expr="row_item[13]" type="datetime" optional>,\n
<dtml-sqlvar expr="row_item[14]" type="datetime" optional>,\n
<dtml-sqlvar expr="row_item[15]" type="float" optional>,\n
<dtml-sqlvar expr="row_item[16]" type="string" optional>,\n
<dtml-sqlvar expr="row_item[17]" type="string" optional>,\n
<dtml-sqlvar expr="row_item[18]" type="string" optional>,\n
<dtml-sqlvar expr="row_item[19]" type="string" optional>\n
)\n
<dtml-if sequence-end><dtml-else>,</dtml-if>\n
</dtml-in>\n
</dtml-if>\n
</dtml-let>\n
]]></string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -92,7 +92,6 @@ WHERE\n ...@@ -92,7 +92,6 @@ WHERE\n
;\n ;\n
\n \n
<dtml-var "\'\\0\'">\n <dtml-var "\'\\0\'">\n
\n
<dtml-let row_list="[]" uid_dict="{}">\n <dtml-let row_list="[]" uid_dict="{}">\n
<dtml-in prefix="loop" expr="_.range(_.len(uid))">\n <dtml-in prefix="loop" expr="_.range(_.len(uid))">\n
<dtml-if "not(isInventoryMovement[loop_item]) and isMovement[loop_item] and getResourceUid[loop_item]">\n <dtml-if "not(isInventoryMovement[loop_item]) and isMovement[loop_item] and getResourceUid[loop_item]">\n
...@@ -152,7 +151,6 @@ WHERE\n ...@@ -152,7 +151,6 @@ WHERE\n
</dtml-if>\n </dtml-if>\n
</dtml-if>\n </dtml-if>\n
</dtml-in> \n </dtml-in> \n
\n
<dtml-if "row_list">\n <dtml-if "row_list">\n
INSERT INTO\n INSERT INTO\n
stock\n stock\n
......
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>arguments_src</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_create_inventory</string> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string>CREATE TABLE `inventory` (\n
`uid` BIGINT(20) UNSIGNED NOT NULL,\n
`node_uid` BIGINT(20) UNSIGNED NULL,\n
`section_uid` BIGINT(20) UNSIGNED NULL,\n
`payment_uid` BIGINT(20) UNSIGNED NULL,\n
`date` DATETIME NOT NULL,\n
`is_full_inventory` BOOL NOT NULL DEFAULT FALSE,\n
`simulation_state` VARCHAR(255) NOT NULL DEFAULT \'\',\n
PRIMARY KEY `uid` (`uid`),\n
KEY `node_index` (`is_full_inventory`, `simulation_state`, `node_uid`),\n
KEY `section_index` (`is_full_inventory`, `simulation_state`, `section_uid`),\n
KEY `payment_index` (`is_full_inventory`, `simulation_state`, `payment_uid`)\n
) ENGINE=InnoDB</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="SQL" module="Products.ZSQLMethods.SQL"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>arguments_src</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>connection_id</string> </key>
<value> <string>erp5_sql_connection</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>z_create_inventory_stock</string> </value>
</item>
<item>
<key> <string>src</string> </key>
<value> <string>CREATE TABLE `inventory_stock` (\n
`uid` BIGINT UNSIGNED NOT NULL,\n
`order_id` TINYINT UNSIGNED NOT NULL,\n
`node_uid` BIGINT UNSIGNED,\n
`section_uid` BIGINT UNSIGNED,\n
`payment_uid` BIGINT UNSIGNED,\n
`function_uid` BIGINT UNSIGNED,\n
`project_uid` BIGINT UNSIGNED,\n
`funding_uid` BIGINT UNSIGNED,\n
`payment_request_uid` BIGINT UNSIGNED,\n
`mirror_section_uid` BIGINT UNSIGNED,\n
`mirror_node_uid` BIGINT UNSIGNED,\n
`resource_uid` BIGINT UNSIGNED,\n
`quantity` real ,\n
`is_accountable` BOOLEAN,\n
`date` datetime,\n
`mirror_date` datetime,\n
`total_price` real ,\n
`portal_type` VARCHAR(255),\n
`simulation_state` varchar(255) default \'\',\n
`variation_text` VARCHAR(255),\n
`sub_variation_text` VARCHAR(255),\n
PRIMARY KEY (`uid`, `order_id`),\n
KEY `quantity` (`quantity`),\n
KEY `section_uid` (`section_uid`),\n
KEY `mirror_section_uid` (`mirror_section_uid`),\n
KEY `mirror_node_uid` (`mirror_node_uid`),\n
KEY `node_uid` (`node_uid`),\n
KEY `payment_uid` (`payment_uid`),\n
KEY `function_uid` (`function_uid`),\n
KEY `project_uid` (`project_uid`),\n
KEY `simulation_state` (`simulation_state`),\n
KEY `resource_node_uid` (`resource_uid`, `node_uid`),\n
KEY `resource_section_node_uid` (`resource_uid`, `section_uid`, `node_uid`, `simulation_state`),\n
KEY `date` (`date`)\n
) ENGINE=InnoDB\n
</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
<key>catalog</key> <key>catalog</key>
<key>category</key> <key>category</key>
<key>delivery</key> <key>delivery</key>
<key>inventory</key>
<key>inventory_stock</key>
<key>item</key> <key>item</key>
<key>predicate</key> <key>predicate</key>
<key>predicate_category</key> <key>predicate_category</key>
......
...@@ -12,9 +12,6 @@ delivery.start_date_range_min ...@@ -12,9 +12,6 @@ delivery.start_date_range_min
delivery.stop_date delivery.stop_date
delivery.stop_date_range_max delivery.stop_date_range_max
delivery.stop_date_range_min delivery.stop_date_range_min
inventory.date
inventory_stock.date
inventory_stock.mirror_date
item.date item.date
mirror_date mirror_date
modification_date modification_date
......
...@@ -7,8 +7,6 @@ erp5_mysql_innodb/z0_drop_alarm ...@@ -7,8 +7,6 @@ erp5_mysql_innodb/z0_drop_alarm
erp5_mysql_innodb/z0_drop_catalog erp5_mysql_innodb/z0_drop_catalog
erp5_mysql_innodb/z0_drop_category erp5_mysql_innodb/z0_drop_category
erp5_mysql_innodb/z0_drop_delivery erp5_mysql_innodb/z0_drop_delivery
erp5_mysql_innodb/z0_drop_inventory
erp5_mysql_innodb/z0_drop_inventory_stock
erp5_mysql_innodb/z0_drop_item erp5_mysql_innodb/z0_drop_item
erp5_mysql_innodb/z0_drop_measure erp5_mysql_innodb/z0_drop_measure
erp5_mysql_innodb/z0_drop_predicate erp5_mysql_innodb/z0_drop_predicate
...@@ -23,8 +21,6 @@ erp5_mysql_innodb/z0_drop_translation ...@@ -23,8 +21,6 @@ erp5_mysql_innodb/z0_drop_translation
erp5_mysql_innodb/z0_drop_versioning erp5_mysql_innodb/z0_drop_versioning
erp5_mysql_innodb/z0_uncatalog_alarm erp5_mysql_innodb/z0_uncatalog_alarm
erp5_mysql_innodb/z0_uncatalog_category erp5_mysql_innodb/z0_uncatalog_category
erp5_mysql_innodb/z0_uncatalog_inventory
erp5_mysql_innodb/z0_uncatalog_inventory_stock
erp5_mysql_innodb/z0_uncatalog_item erp5_mysql_innodb/z0_uncatalog_item
erp5_mysql_innodb/z0_uncatalog_measure erp5_mysql_innodb/z0_uncatalog_measure
erp5_mysql_innodb/z0_uncatalog_predicate erp5_mysql_innodb/z0_uncatalog_predicate
...@@ -35,8 +31,6 @@ erp5_mysql_innodb/z0_uncatalog_transformation ...@@ -35,8 +31,6 @@ erp5_mysql_innodb/z0_uncatalog_transformation
erp5_mysql_innodb/z0_uncatalog_versioning erp5_mysql_innodb/z0_uncatalog_versioning
erp5_mysql_innodb/z_catalog_alarm_list erp5_mysql_innodb/z_catalog_alarm_list
erp5_mysql_innodb/z_catalog_delivery_list erp5_mysql_innodb/z_catalog_delivery_list
erp5_mysql_innodb/z_catalog_inventory_list
erp5_mysql_innodb/z_catalog_inventory_stock_list
erp5_mysql_innodb/z_catalog_item_list erp5_mysql_innodb/z_catalog_item_list
erp5_mysql_innodb/z_catalog_measure_list erp5_mysql_innodb/z_catalog_measure_list
erp5_mysql_innodb/z_catalog_movement_category_list erp5_mysql_innodb/z_catalog_movement_category_list
...@@ -58,8 +52,6 @@ erp5_mysql_innodb/z_create_alarm ...@@ -58,8 +52,6 @@ erp5_mysql_innodb/z_create_alarm
erp5_mysql_innodb/z_create_catalog erp5_mysql_innodb/z_create_catalog
erp5_mysql_innodb/z_create_category erp5_mysql_innodb/z_create_category
erp5_mysql_innodb/z_create_delivery erp5_mysql_innodb/z_create_delivery
erp5_mysql_innodb/z_create_inventory
erp5_mysql_innodb/z_create_inventory_stock
erp5_mysql_innodb/z_create_item erp5_mysql_innodb/z_create_item
erp5_mysql_innodb/z_create_measure erp5_mysql_innodb/z_create_measure
erp5_mysql_innodb/z_create_predicate erp5_mysql_innodb/z_create_predicate
......
...@@ -2,8 +2,6 @@ catalog ...@@ -2,8 +2,6 @@ catalog
category category
alarm alarm
delivery delivery
inventory
inventory_stock
item item
predicate predicate
predicate_category predicate_category
......
...@@ -184,7 +184,7 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -184,7 +184,7 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
inventory_list.append(inventory) inventory_list.append(inventory)
sequence.edit(inventory_list = inventory_list) sequence.edit(inventory_list = inventory_list)
def createInventory(self, sequence=None): def createInventory(self, sequence=None, full=False):
""" """
""" """
portal = self.getPortal() portal = self.getPortal()
...@@ -193,7 +193,8 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -193,7 +193,8 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
inventory = inventory_module.newContent(portal_type = self.inventory_portal_type) inventory = inventory_module.newContent(portal_type = self.inventory_portal_type)
inventory.edit(destination_value = sequence.get('node'), inventory.edit(destination_value = sequence.get('node'),
destination_section_value = sequence.get('section'), destination_section_value = sequence.get('section'),
start_date = DateTime() + 1 start_date = DateTime() + 1,
full_inventory=full,
) )
inventory_list.append(inventory) inventory_list.append(inventory)
sequence.edit(inventory_list=inventory_list) sequence.edit(inventory_list=inventory_list)
...@@ -213,6 +214,66 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -213,6 +214,66 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
inventory_list.append(inventory) inventory_list.append(inventory)
sequence.edit(inventory_list=inventory_list) sequence.edit(inventory_list=inventory_list)
def stepCreateFullInventory(self, sequence=None, sequence_list=None, **kw):
"""
Create a full Inventory object for Inventory Module testing
"""
inventory = self.createInventory(sequence=sequence)
inventory_list = sequence.get('inventory_list',[])
inventory.edit(full_inventory=True)
inventory_line = inventory.newContent(
portal_type = self.inventory_line_portal_type,
resource_value = sequence.get("second_resource"),
inventory = 101)
inventory.deliver()
inventory_list.append(inventory)
sequence.edit(inventory_list=inventory_list)
def stepCreatePartialInventoryMultipleResource(self, sequence=None, sequence_list=None, **kw):
"""
Create a partial inventory object for one resource
"""
inventory = self.createInventory(sequence=sequence)
inventory_list = sequence.get('inventory_list',[])
inventory.edit(full_inventory=False)
inventory_line = inventory.newContent(
portal_type = self.inventory_line_portal_type,
resource_value = sequence.get("second_resource"),
inventory = 101)
inventory.deliver()
inventory_list.append(inventory)
sequence.edit(inventory_list=inventory_list)
def stepTestPartialInventoryMultipleResource(self, sequence=None, sequence_list=None, **kw):
"""
Test partial inventory behavior with multiple resource
"""
inventory_list = sequence.get('inventory_list')
simulation = self.getPortal().portal_simulation
# First resource, must not have changed
inventory = simulation.getCurrentInventory(
resource = sequence.get("resource").getRelativeUrl(),
section = sequence.get('section').getRelativeUrl(),
node = sequence.get('node').getRelativeUrl(),
)
self.assertEquals(inventory, 100.,
'section=%s, node=%s' % (
sequence.get('section').getRelativeUrl(),
sequence.get('node').getRelativeUrl()))
# second resource, must be 101
inventory = simulation.getCurrentInventory(
resource = sequence.get("second_resource").getRelativeUrl(),
section = sequence.get('section').getRelativeUrl(),
node = sequence.get('node').getRelativeUrl(),
)
self.assertEquals(inventory, 101.,
'section=%s, node=%s' % (
sequence.get('section').getRelativeUrl(),
sequence.get('node').getRelativeUrl()))
def stepCreateSingleVariatedInventory(self, sequence=None, sequence_list=None, **kw): def stepCreateSingleVariatedInventory(self, sequence=None, sequence_list=None, **kw):
""" """
Create a single Inventory object for Inventory Module testing Create a single Inventory object for Inventory Module testing
...@@ -231,19 +292,32 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -231,19 +292,32 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
variation_category_list = category_list, variation_category_list = category_list,
mapped_value_property_list = ['quantity'], mapped_value_property_list = ['quantity'],
) )
category_list = sequence.get('variation_2') # When checking the not full inventory function, quantity must remain the same if
# no inventory line defined for a variation
inventory.deliver()
def stepCreateFullVariatedInventory(self, sequence=None, sequence_list=None, **kw):
"""
Create a single full Inventory object for Inventory Module testing
"""
inventory = self.createInventory(sequence=sequence, full=True)
inventory_line = inventory.newContent(portal_type = self.inventory_line_portal_type) inventory_line = inventory.newContent(portal_type = self.inventory_line_portal_type)
category_list = sequence.get('variation_1')
inventory_line.edit(resource_value = sequence.get('resource'), inventory_line.edit(resource_value = sequence.get('resource'),
variation_category_list=category_list variation_category_list=category_list
) )
cell = inventory_line.newCell(base_id='movement',*category_list) cell = inventory_line.newCell(base_id='movement',*category_list)
quantity=0
cell.edit( cell.edit(
quantity = quantity, quantity = 55,
predicate_category_list = category_list, predicate_category_list = category_list,
variation_category_list = category_list, variation_category_list = category_list,
mapped_value_property_list = ['quantity'], mapped_value_property_list = ['quantity'],
) )
inventory_line = inventory.newContent(
portal_type = self.inventory_line_portal_type,
resource_value = sequence.get("second_resource"),
inventory = 101)
inventory.deliver() inventory.deliver()
def stepCreatePackingListForModule(self, sequence=None, def stepCreatePackingListForModule(self, sequence=None,
...@@ -1762,10 +1836,11 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -1762,10 +1836,11 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
variation_category_list.sort() variation_category_list.sort()
variation_text = '\n'.join(variation_category_list) variation_text = '\n'.join(variation_category_list)
inventory = simulation.getCurrentInventory( inventory = simulation.getCurrentInventory(
section=sequence.get('section').getRelativeUrl(), resource = sequence.get("resource").getRelativeUrl(),
node=sequence.get('node').getRelativeUrl(), section = sequence.get('section').getRelativeUrl(),
variation_text=variation_text node = sequence.get('node').getRelativeUrl(),
) variation_text = variation_text
)
self.assertEquals(inventory, quantity) self.assertEquals(inventory, quantity)
def stepTestInitialVariatedInventory(self, sequence=None, sequence_list=None, **kw): def stepTestInitialVariatedInventory(self, sequence=None, sequence_list=None, **kw):
...@@ -1806,10 +1881,51 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -1806,10 +1881,51 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
self.checkVariatedInventory(variation_category_list=variation_category_list, self.checkVariatedInventory(variation_category_list=variation_category_list,
quantity=quantity,sequence=sequence) quantity=quantity,sequence=sequence)
variation_category_list = sequence.get('variation_2') variation_category_list = sequence.get('variation_2')
quantity = 3
self.checkVariatedInventory(variation_category_list=variation_category_list,
quantity=quantity,sequence=sequence)
def stepTestVariatedInventoryNonDefaultQuantityUnitAfterInventory(self, sequence=None, sequence_list=None, **kw):
"""
Test Inventory Module behavior
"""
resource = sequence.get('resource')
variation_category_list = sequence.get('variation_1')
quantity = 5
self.checkVariatedInventory(variation_category_list=variation_category_list,
quantity=quantity,sequence=sequence)
variation_category_list = sequence.get('variation_2')
quantity = 300
self.checkVariatedInventory(variation_category_list=variation_category_list,
quantity=quantity,sequence=sequence)
def stepTestFullVariatedInventory(self, sequence=None, sequence_list=None, **kw):
"""
Test full inventory with variated resource
"""
resource = sequence.get('resource')
variation_category_list = sequence.get('variation_1')
# Test first resource
quantity = 55
self.checkVariatedInventory(variation_category_list=variation_category_list,
quantity=quantity,sequence=sequence)
variation_category_list = sequence.get('variation_2')
quantity = 0 quantity = 0
self.checkVariatedInventory(variation_category_list=variation_category_list, self.checkVariatedInventory(variation_category_list=variation_category_list,
quantity=quantity,sequence=sequence) quantity=quantity,sequence=sequence)
# second resource, must be 101
simulation = self.getPortal().portal_simulation
inventory = simulation.getCurrentInventory(
resource = sequence.get("second_resource").getRelativeUrl(),
section = sequence.get('section').getRelativeUrl(),
node = sequence.get('node').getRelativeUrl(),
)
self.assertEquals(inventory, 101.,
'section=%s, node=%s' % (
sequence.get('section').getRelativeUrl(),
sequence.get('node').getRelativeUrl()))
def stepTestInventoryModule(self, sequence=None, sequence_list=None, **kw): def stepTestInventoryModule(self, sequence=None, sequence_list=None, **kw):
""" """
Test Inventory Module behavior Test Inventory Module behavior
...@@ -1832,6 +1948,36 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -1832,6 +1948,36 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
step += 1 step += 1
sequence.edit(step=step) sequence.edit(step=step)
def stepTestFullInventory(self, sequence=None, sequence_list=None, **kw):
"""
Test Full inventory behavior
"""
inventory_list = sequence.get('inventory_list')
simulation = self.getPortal().portal_simulation
# First resource, must be zero
inventory = simulation.getCurrentInventory(
resource = sequence.get("resource").getRelativeUrl(),
section = sequence.get('section').getRelativeUrl(),
node = sequence.get('node').getRelativeUrl(),
)
self.assertEquals(inventory, 0.,
'section=%s, node=%s' % (
sequence.get('section').getRelativeUrl(),
sequence.get('node').getRelativeUrl()))
# second resource, must be 101
inventory = simulation.getCurrentInventory(
resource = sequence.get("second_resource").getRelativeUrl(),
section = sequence.get('section').getRelativeUrl(),
node = sequence.get('node').getRelativeUrl(),
)
self.assertEquals(inventory, 101.,
'section=%s, node=%s' % (
sequence.get('section').getRelativeUrl(),
sequence.get('node').getRelativeUrl()))
def stepModifyFirstInventory(self, sequence=None, sequence_list=None, **kw): def stepModifyFirstInventory(self, sequence=None, sequence_list=None, **kw):
""" """
Modify the first entered Inventory, to test the quantity change Modify the first entered Inventory, to test the quantity change
...@@ -1845,6 +1991,27 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -1845,6 +1991,27 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
quantity=sum([x.getQuantity() for x in aggregate_value_list])) quantity=sum([x.getQuantity() for x in aggregate_value_list]))
def stepCreateNotVariatedSecondResource(self,sequence=None,
sequence_list=None,
**kw):
"""
Create a second resource with no variation
"""
portal = self.getPortal()
resource_module = portal.getDefaultModule(self.resource_portal_type)
resource = resource_module.newContent(portal_type=self.resource_portal_type)
resource.edit(
title = "NotVariatedSecondResource%s" % resource.getId(),
industrial_phase_list=["phase1", "phase2"],
product_line = 'apparel'
)
sequence.edit( second_resource = resource )
resource_list = sequence.get('resource_list',default=[])
resource_list.append(resource)
sequence.edit( resource_list = resource_list )
def test_01_getInventory(self, quiet=0, run=run_all_test): def test_01_getInventory(self, quiet=0, run=run_all_test):
""" """
Test the getInventory methods Test the getInventory methods
...@@ -1966,7 +2133,7 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -1966,7 +2133,7 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
stepTestInitialVariatedNonDefaultQuantityUnitInventory \ stepTestInitialVariatedNonDefaultQuantityUnitInventory \
stepCreateSingleVariatedInventory \ stepCreateSingleVariatedInventory \
stepTic \ stepTic \
stepTestVariatedInventoryAfterInventory \ stepTestVariatedInventoryNonDefaultQuantityUnitAfterInventory \
' '
sequence_list.addSequenceString(sequence_string) sequence_list.addSequenceString(sequence_string)
sequence_list.play(self) sequence_list.play(self)
...@@ -2017,6 +2184,83 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase): ...@@ -2017,6 +2184,83 @@ class TestInventory(TestOrderMixin, ERP5TypeTestCase):
resource_uid=product.getUid()), resource_uid=product.getUid()),
0) 0)
def test_06_FullInventory(self, quiet=0, run=run_all_test):
"""
Test the full inventory behavior
"""
if not run: return
sequence_list = SequenceList()
sequence_string = 'stepCreateOrganisationsForModule \
stepCreateNotVariatedResource \
stepCreateNotVariatedSecondResource \
stepCreateItemList \
stepCreatePackingListForModule \
stepTic \
stepCreatePackingListLine \
stepTic \
stepDeliverPackingList \
stepTic \
stepCreateFullInventory \
stepTic \
stepTestFullInventory \
'
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def test_07_FullVariatedInventory(self, quiet=0, run=run_all_test):
"""
Test the full inventory behavior with variated resource
"""
if not run: return
sequence_list = SequenceList()
sequence_string = 'stepCreateOrganisationsForModule \
stepCreateVariatedResource \
stepCreateNotVariatedSecondResource \
stepCreateItemList \
stepCreatePackingListForModule \
stepTic \
stepCreateVariatedPackingListLine \
stepTic \
stepDeliverPackingList \
stepTic \
stepCreateFullVariatedInventory \
stepTic \
stepTestFullVariatedInventory \
'
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def test_08_PartialInventoryMultipleResource(self, quiet=0, run=run_all_test):
"""
Test behaviour of partial inventory with multiple resource
defining inventory of resource B must not modify inventory of resource A
"""
if not run: return
sequence_list = SequenceList()
sequence_string = 'stepCreateOrganisationsForModule \
stepCreateNotVariatedResource \
stepCreateNotVariatedSecondResource \
stepCreateItemList \
stepCreatePackingListForModule \
stepTic \
stepCreatePackingListLine \
stepTic \
stepDeliverPackingList \
stepTic \
stepCreatePartialInventoryMultipleResource \
stepTic \
stepTestPartialInventoryMultipleResource \
'
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestInventory)) suite.addTest(unittest.makeSuite(TestInventory))
......
...@@ -38,14 +38,13 @@ import unittest ...@@ -38,14 +38,13 @@ import unittest
from AccessControl.SecurityManagement import newSecurityManager from AccessControl.SecurityManagement import newSecurityManager
from DateTime import DateTime from DateTime import DateTime
from Testing import ZopeTestCase from MySQLdb import ProgrammingError
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import reindex from Products.ERP5Type.tests.utils import reindex
from Products.ERP5Type.tests.backportUnittest import expectedFailure from Products.ERP5Type.tests.backportUnittest import expectedFailure
from Products.DCWorkflow.DCWorkflow import ValidationFailed from Products.ERP5.Tool.SimulationTool import MYSQL_MIN_DATETIME_RESOLUTION
from Products.ERP5Type.Base import _aq_reset
from Products.ERP5Type.tests.utils import createZODBPythonScript
class InventoryAPITestCase(ERP5TypeTestCase): class InventoryAPITestCase(ERP5TypeTestCase):
"""Base class for Inventory API Tests {{{ """Base class for Inventory API Tests {{{
...@@ -98,7 +97,7 @@ class InventoryAPITestCase(ERP5TypeTestCase): ...@@ -98,7 +97,7 @@ class InventoryAPITestCase(ERP5TypeTestCase):
self.portal.newContent(portal_type='Folder', self.portal.newContent(portal_type='Folder',
id='testing_folder') id='testing_folder')
self.folder = self.portal.testing_folder self.folder = self.portal.testing_folder
self.section = self._makeOrganisation(title='Section') self.section = self._makeOrganisation(title='Section')
self.other_section = self._makeOrganisation(title='Other Section') self.other_section = self._makeOrganisation(title='Other Section')
self.node = self._makeOrganisation(title='Node') self.node = self._makeOrganisation(title='Node')
...@@ -143,7 +142,7 @@ class InventoryAPITestCase(ERP5TypeTestCase): ...@@ -143,7 +142,7 @@ class InventoryAPITestCase(ERP5TypeTestCase):
'Associate', 'Auditor', 'Author'], []) 'Associate', 'Auditor', 'Author'], [])
user = uf.getUserById('alex').__of__(uf) user = uf.getUserById('alex').__of__(uf)
newSecurityManager(None, user) newSecurityManager(None, user)
def createCategories(self): def createCategories(self):
"""Create the categories for our test. """ """Create the categories for our test. """
# create categories # create categories
...@@ -163,7 +162,7 @@ class InventoryAPITestCase(ERP5TypeTestCase): ...@@ -163,7 +162,7 @@ class InventoryAPITestCase(ERP5TypeTestCase):
self.getCategoryTool().restrictedTraverse(cat_string), self.getCategoryTool().restrictedTraverse(cat_string),
cat_string) cat_string)
self.tic() self.tic()
def getNeededCategoryList(self): def getNeededCategoryList(self):
"""return a list of categories that should be created.""" """return a list of categories that should be created."""
return ( 'region/level1/level2', return ( 'region/level1/level2',
...@@ -177,15 +176,16 @@ class InventoryAPITestCase(ERP5TypeTestCase): ...@@ -177,15 +176,16 @@ class InventoryAPITestCase(ERP5TypeTestCase):
'function/function1/function2', 'function/function1/function2',
# we create a huge group category for consolidation tests # we create a huge group category for consolidation tests
) + self.GROUP_CATEGORIES + self.VARIATION_CATEGORIES ) + self.GROUP_CATEGORIES + self.VARIATION_CATEGORIES
def getBusinessTemplateList(self): def getBusinessTemplateList(self):
""" erp5_trade is required for transit_simulation_state """ erp5_trade is required for transit_simulation_state
erp5_apparel is required for item erp5_apparel is required for item
""" """
return ('erp5_base', 'erp5_pdm', 'erp5_dummy_movement', 'erp5_simulation', return ('erp5_core_proxy_field_legacy', 'erp5_base', 'erp5_pdm',
'erp5_dummy_movement', 'erp5_simulation',
'erp5_trade', 'erp5_apparel', 'erp5_project', 'erp5_trade', 'erp5_apparel', 'erp5_project',
'erp5_configurator_standard_trade_template', 'erp5_configurator_standard_trade_template',
'erp5_simulation_test') 'erp5_simulation_test', 'erp5_stock_cache')
# TODO: move this to a base class {{{ # TODO: move this to a base class {{{
@reindex @reindex
...@@ -210,7 +210,7 @@ class InventoryAPITestCase(ERP5TypeTestCase): ...@@ -210,7 +210,7 @@ class InventoryAPITestCase(ERP5TypeTestCase):
portal_type='Sale Packing List', portal_type='Sale Packing List',
specialise=self.business_process, specialise=self.business_process,
**kw) **kw)
@reindex @reindex
def _makeSaleInvoice(self, created_by_builder=0, **kw): def _makeSaleInvoice(self, created_by_builder=0, **kw):
"""Creates a sale invoice.""" """Creates a sale invoice."""
...@@ -245,7 +245,7 @@ class InventoryAPITestCase(ERP5TypeTestCase): ...@@ -245,7 +245,7 @@ class InventoryAPITestCase(ERP5TypeTestCase):
kw.setdefault('resource_value', self.resource) kw.setdefault('resource_value', self.resource)
mvt.edit(**kw) mvt.edit(**kw)
return mvt return mvt
@reindex @reindex
def _makeSimulationMovement(self, **kw): def _makeSimulationMovement(self, **kw):
"""Creates a simulation movement. """Creates a simulation movement.
...@@ -316,7 +316,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -316,7 +316,7 @@ class TestInventory(InventoryAPITestCase):
self.assertInventoryEquals(100, section_category='group/level1') self.assertInventoryEquals(100, section_category='group/level1')
self.assertInventoryEquals(100, section_category='group/level1/level2') self.assertInventoryEquals(100, section_category='group/level1/level2')
self.assertInventoryEquals(0, section_category='group/anotherlevel') self.assertInventoryEquals(0, section_category='group/anotherlevel')
# section category can be a list # section category can be a list
self.assertInventoryEquals(100, self.assertInventoryEquals(100,
section_category=['group/anotherlevel', 'group/level1']) section_category=['group/anotherlevel', 'group/level1'])
...@@ -329,7 +329,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -329,7 +329,7 @@ class TestInventory(InventoryAPITestCase):
self.tic() self.tic()
self.assertInventoryEquals(100, self.assertInventoryEquals(100,
section_category_strict_membership=['group/level1']) section_category_strict_membership=['group/level1'])
# non existing values to section_category are not silently ignored, but # non existing values to section_category are not silently ignored, but
# raises an exception # raises an exception
self.assertRaises(ValueError, self.assertRaises(ValueError,
...@@ -356,7 +356,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -356,7 +356,7 @@ class TestInventory(InventoryAPITestCase):
self.tic() self.tic()
self.assertInventoryEquals(100, self.assertInventoryEquals(100,
mirror_section_category_strict_membership=['group/level1']) mirror_section_category_strict_membership=['group/level1'])
# non existing values to section_category are not silently ignored, but # non existing values to section_category are not silently ignored, but
# raises an exception # raises an exception
self.assertRaises(ValueError, self.assertRaises(ValueError,
...@@ -375,7 +375,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -375,7 +375,7 @@ class TestInventory(InventoryAPITestCase):
self.tic() self.tic()
self.assertInventoryEquals(100, self.assertInventoryEquals(100,
node_category_strict_membership=['group/level1']) node_category_strict_membership=['group/level1'])
def test_Function(self): def test_Function(self):
"""Tests inventory on function""" """Tests inventory on function"""
self._makeMovement(quantity=100, destination_function='function/function1') self._makeMovement(quantity=100, destination_function='function/function1')
...@@ -631,7 +631,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -631,7 +631,7 @@ class TestInventory(InventoryAPITestCase):
# ... is equivalent to node_category # ... is equivalent to node_category
self.assertInventoryEquals(total_quantity, self.assertInventoryEquals(total_quantity,
node_category=category.getRelativeUrl()) node_category=category.getRelativeUrl())
@expectedFailure @expectedFailure
def test_DoubleCategoryMembershipSectionCategory(self): def test_DoubleCategoryMembershipSectionCategory(self):
"""Tests inventory on section category, when the section is twice member\ """Tests inventory on section category, when the section is twice member\
...@@ -655,7 +655,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -655,7 +655,7 @@ class TestInventory(InventoryAPITestCase):
self.assertInventoryEquals(100, self.assertInventoryEquals(100,
section_category_strict_membership=['group/level1/level2']) section_category_strict_membership=['group/level1/level2'])
self.assertInventoryEquals(100, section_uid=self.section.getUid()) self.assertInventoryEquals(100, section_uid=self.section.getUid())
def testPrecision(self): def testPrecision(self):
# getInventory supports a precision= argument to specify the precision to # getInventory supports a precision= argument to specify the precision to
# round # round
...@@ -670,7 +670,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -670,7 +670,7 @@ class TestInventory(InventoryAPITestCase):
getInventoryAssetPrice(precision=3, getInventoryAssetPrice(precision=3,
node_uid=self.node.getUid()), node_uid=self.node.getUid()),
places=3) places=3)
def testPrecisionAndFloatRoundingIssues(self): def testPrecisionAndFloatRoundingIssues(self):
# sum([0.1] * 10) != 1.0 but this is not a problem here # sum([0.1] * 10) != 1.0 but this is not a problem here
getInventoryAssetPrice = self.getSimulationTool().getInventoryAssetPrice getInventoryAssetPrice = self.getSimulationTool().getInventoryAssetPrice
...@@ -680,7 +680,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -680,7 +680,7 @@ class TestInventory(InventoryAPITestCase):
self.assertInventoryEquals(0, precision=2, node_uid=self.node.getUid()) self.assertInventoryEquals(0, precision=2, node_uid=self.node.getUid())
self.assertEquals(0, getInventoryAssetPrice(precision=2, self.assertEquals(0, getInventoryAssetPrice(precision=2,
node_uid=self.node.getUid())) node_uid=self.node.getUid()))
def test_OmitInputOmitOutput(self): def test_OmitInputOmitOutput(self):
self._makeMovement(quantity=1, price=1) self._makeMovement(quantity=1, price=1)
self._makeMovement(quantity=-1, price=1) self._makeMovement(quantity=-1, price=1)
...@@ -699,7 +699,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -699,7 +699,7 @@ class TestInventory(InventoryAPITestCase):
self._makeMovement(quantity=2, price=1, source_section_value=None) self._makeMovement(quantity=2, price=1, source_section_value=None)
self.assertInventoryEquals(-3, node_uid=self.node.getUid(), omit_input=1) self.assertInventoryEquals(-3, node_uid=self.node.getUid(), omit_input=1)
self.assertInventoryEquals(3, node_uid=self.node.getUid(), omit_output=1) self.assertInventoryEquals(3, node_uid=self.node.getUid(), omit_output=1)
def test_OmitInputOmitOutputWithDifferentSections(self): def test_OmitInputOmitOutputWithDifferentSections(self):
self._makeMovement(quantity=2, price=1) self._makeMovement(quantity=2, price=1)
self._makeMovement(quantity=-3, price=1, self._makeMovement(quantity=-3, price=1,
...@@ -716,7 +716,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -716,7 +716,7 @@ class TestInventory(InventoryAPITestCase):
self.assertInventoryEquals(0, node_uid=self.node.getUid(), self.assertInventoryEquals(0, node_uid=self.node.getUid(),
section_uid=self.other_section.getUid(), section_uid=self.other_section.getUid(),
omit_output=1) omit_output=1)
def test_OmitInputOmitOutputWithDifferentPayment(self): def test_OmitInputOmitOutputWithDifferentPayment(self):
# simple case # simple case
self._makeMovement(quantity=2, price=1, self._makeMovement(quantity=2, price=1,
...@@ -753,7 +753,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -753,7 +753,7 @@ class TestInventory(InventoryAPITestCase):
# omit_output & omit_input return nothing in that case # omit_output & omit_input return nothing in that case
self.assertInventoryEquals(0, node_uid=self.node.getUid(), self.assertInventoryEquals(0, node_uid=self.node.getUid(),
omit_input=1, omit_output=1) omit_input=1, omit_output=1)
def test_OmitInputOmitOutputWithDifferentPaymentSameNodeSameSection(self): def test_OmitInputOmitOutputWithDifferentPaymentSameNodeSameSection(self):
self._makeMovement(quantity=2, price=1, self._makeMovement(quantity=2, price=1,
source_value=self.node, source_value=self.node,
...@@ -788,7 +788,7 @@ class TestInventory(InventoryAPITestCase): ...@@ -788,7 +788,7 @@ class TestInventory(InventoryAPITestCase):
def test_TimeZone(self): def test_TimeZone(self):
""" """
Check that getInventory support DateTime parameter with Check that getInventory support DateTime parameter with
timezone timezone
""" """
date_gmt_1 = DateTime('2005/12/01 GMT+9') date_gmt_1 = DateTime('2005/12/01 GMT+9')
...@@ -808,7 +808,7 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -808,7 +808,7 @@ class TestInventoryList(InventoryAPITestCase):
"""Tests getInventoryList methods. """Tests getInventoryList methods.
""" """
def test_ReturnedTypeIsList(self): def test_ReturnedTypeIsList(self):
"""Inventory List returns a sequence object""" """Inventory List returns a sequence object"""
getInventoryList = self.getSimulationTool().getInventoryList getInventoryList = self.getSimulationTool().getInventoryList
inventory_list = getInventoryList() inventory_list = getInventoryList()
self.assertEquals(str(inventory_list.__class__), self.assertEquals(str(inventory_list.__class__),
...@@ -1033,7 +1033,7 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1033,7 +1033,7 @@ class TestInventoryList(InventoryAPITestCase):
self._makeMovement(quantity=7, use='use2') self._makeMovement(quantity=7, use='use2')
self._makeMovement(quantity=4, use='use2') self._makeMovement(quantity=4, use='use2')
# note that grouping by related key only make sense if you group by strict # note that grouping by related key only make sense if you group by strict
# memebership related keys # membership related keys
inventory_list = getInventoryList(node_uid=(self.node.getUid(), inventory_list = getInventoryList(node_uid=(self.node.getUid(),
self.other_node.getUid()), self.other_node.getUid()),
group_by=('strict_use_uid', )) group_by=('strict_use_uid', ))
...@@ -1042,7 +1042,7 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1042,7 +1042,7 @@ class TestInventoryList(InventoryAPITestCase):
if r.getObject().getUse() == 'use1'][0].inventory, 5) if r.getObject().getUse() == 'use1'][0].inventory, 5)
self.assertEquals([r for r in inventory_list self.assertEquals([r for r in inventory_list
if r.getObject().getUse() == 'use2'][0].inventory, 11) if r.getObject().getUse() == 'use2'][0].inventory, 11)
# in such case, it's interesting to pass a select expression, to be have on # in such case, it's interesting to pass a select expression, to be have on
# brain the information of which category is used # brain the information of which category is used
inventory_list = getInventoryList(node_uid=(self.node.getUid(), inventory_list = getInventoryList(node_uid=(self.node.getUid(),
...@@ -1131,7 +1131,7 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1131,7 +1131,7 @@ class TestInventoryList(InventoryAPITestCase):
self.assertEquals(0, len(getInventoryList(node_uid=self.node.getUid(), self.assertEquals(0, len(getInventoryList(node_uid=self.node.getUid(),
omit_input=1, omit_input=1,
omit_output=1))) omit_output=1)))
def test_OmitAssetIncreaseDecrease(self): def test_OmitAssetIncreaseDecrease(self):
getInventoryList = self.getSimulationTool().getInventoryList getInventoryList = self.getSimulationTool().getInventoryList
m1 = self._makeMovement(quantity=1, price=1) m1 = self._makeMovement(quantity=1, price=1)
...@@ -1241,7 +1241,7 @@ class TestInventoryList(InventoryAPITestCase): ...@@ -1241,7 +1241,7 @@ class TestInventoryList(InventoryAPITestCase):
inventory_list = method(node_uid=node_uid) inventory_list = method(node_uid=node_uid)
self.assertEquals(len(inventory_list), line) self.assertEquals(len(inventory_list), line)
if quantity is not None: if quantity is not None:
self.assertEquals(sum([x.total_quantity for x in inventory_list]), self.assertEquals(sum([x.total_quantity for x in inventory_list]),
quantity) quantity)
makeMovement(quantity=1, simulation_state='ordered') makeMovement(quantity=1, simulation_state='ordered')
checkInventory(line=0, type='Current', destination=1) checkInventory(line=0, type='Current', destination=1)
...@@ -1396,7 +1396,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1396,7 +1396,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
"""Tests Movement history list methods. """Tests Movement history list methods.
""" """
def testReturnedTypeIsList(self): def testReturnedTypeIsList(self):
"""Movement History List returns a sequence object""" """Movement History List returns a sequence object"""
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
mvt_history_list = getMovementHistoryList() mvt_history_list = getMovementHistoryList()
self.assertEquals(str(mvt_history_list.__class__), self.assertEquals(str(mvt_history_list.__class__),
...@@ -1415,17 +1415,17 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1415,17 +1415,17 @@ class TestMovementHistoryList(InventoryAPITestCase):
self.assertEquals(None, mvt_history_list[0].total_price) self.assertEquals(None, mvt_history_list[0].total_price)
def testMovementBothSides(self): def testMovementBothSides(self):
"""Movement History List returns movement from both sides""" """Movement History List returns movement from both sides"""
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
self._makeMovement(quantity=100) self._makeMovement(quantity=100)
# we don't filter, so we have the same movement from both sides. # we don't filter, so we have the same movement from both sides.
self.assertEquals(2, len(getMovementHistoryList())) self.assertEquals(2, len(getMovementHistoryList()))
def testBrainClass(self): def testBrainClass(self):
"""Movement History List uses InventoryListBrain for brains""" """Movement History List uses InventoryListBrain for brains"""
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
self._makeMovement(quantity=100) self._makeMovement(quantity=100)
# maybe this check is too low level (Shared/DC/ZRDB//Results.py, class r) # maybe this check is too low level (Shared/DC/ZRDB//Results.py, class r)
r_bases = getMovementHistoryList()._class.__bases__ r_bases = getMovementHistoryList()._class.__bases__
brain_class = r_bases[2].__name__ brain_class = r_bases[2].__name__
self.assertEquals('MovementHistoryListBrain', brain_class, self.assertEquals('MovementHistoryListBrain', brain_class,
...@@ -1458,7 +1458,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1458,7 +1458,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self.assertEquals(100, mvt_history_list[0].total_quantity) self.assertEquals(100, mvt_history_list[0].total_quantity)
self.assertEquals(self.section.getRelativeUrl(), self.assertEquals(self.section.getRelativeUrl(),
mvt_history_list[0].section_relative_url) mvt_history_list[0].section_relative_url)
def testMirrorSection(self): def testMirrorSection(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
mvt = self._makeMovement(quantity=100) mvt = self._makeMovement(quantity=100)
...@@ -1471,7 +1471,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1471,7 +1471,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
mvt_history_list[0].section_relative_url) mvt_history_list[0].section_relative_url)
self.assertEquals(self.mirror_node.getRelativeUrl(), self.assertEquals(self.mirror_node.getRelativeUrl(),
mvt_history_list[0].node_relative_url) mvt_history_list[0].node_relative_url)
# if we look from the other side, everything is reverted # if we look from the other side, everything is reverted
mvt_history_list = getMovementHistoryList( mvt_history_list = getMovementHistoryList(
section_uid = self.section.getUid()) section_uid = self.section.getUid())
...@@ -1481,7 +1481,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1481,7 +1481,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
mvt_history_list[0].section_relative_url) mvt_history_list[0].section_relative_url)
self.assertEquals(self.node.getRelativeUrl(), self.assertEquals(self.node.getRelativeUrl(),
mvt_history_list[0].node_relative_url) mvt_history_list[0].node_relative_url)
def testDifferentDatesPerSection(self): def testDifferentDatesPerSection(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
start_date = DateTime(2001, 1, 1) start_date = DateTime(2001, 1, 1)
...@@ -1495,7 +1495,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1495,7 +1495,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
# stop_date is for destination # stop_date is for destination
self.assertEquals(stop_date, getMovementHistoryList( self.assertEquals(stop_date, getMovementHistoryList(
section_uid=self.section.getUid())[0].date) section_uid=self.section.getUid())[0].date)
def testNode(self): def testNode(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
mvt = self._makeMovement(quantity=100) mvt = self._makeMovement(quantity=100)
...@@ -1536,7 +1536,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1536,7 +1536,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
# wrong value yields an empty list # wrong value yields an empty list
self.assertEquals(0, len(getMovementHistoryList( self.assertEquals(0, len(getMovementHistoryList(
resource_uid = self.node.getUid()))) resource_uid = self.node.getUid())))
def testSectionCategory(self): def testSectionCategory(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
self.section.setGroup('level1/level2') self.section.setGroup('level1/level2')
...@@ -1553,7 +1553,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1553,7 +1553,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
section_category=section_category) section_category=section_category)
self.assertEquals(len(movement_history_list), 1) self.assertEquals(len(movement_history_list), 1)
self.assertEquals(movement_history_list[0].total_quantity, 100) self.assertEquals(movement_history_list[0].total_quantity, 100)
# again, bad category raises an exception # again, bad category raises an exception
self.assertRaises(ValueError, self.assertRaises(ValueError,
getMovementHistoryList, getMovementHistoryList,
...@@ -1562,7 +1562,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1562,7 +1562,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self.assertEquals(len(getMovementHistoryList( self.assertEquals(len(getMovementHistoryList(
section_category='group/level1', section_category='group/level1',
ignored='argument')), 1) ignored='argument')), 1)
@expectedFailure @expectedFailure
def testDoubleSectionCategory(self): def testDoubleSectionCategory(self):
# it is currently invalid to pass the same category twice to inventory API # it is currently invalid to pass the same category twice to inventory API
...@@ -1632,7 +1632,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1632,7 +1632,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self._makeMovement(quantity=100, self._makeMovement(quantity=100,
start_date=date, start_date=date,
stop_date=date+1) stop_date=date+1)
# from_date takes all movements >= # from_date takes all movements >=
self.assertEquals(len(getMovementHistoryList( self.assertEquals(len(getMovementHistoryList(
from_date=DateTime(2006, 01, 03), from_date=DateTime(2006, 01, 03),
section_uid=self.section.getUid())), 2) section_uid=self.section.getUid())), 2)
...@@ -1717,7 +1717,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1717,7 +1717,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
from_date=DateTime(2006, 01, 02), from_date=DateTime(2006, 01, 02),
to_date=DateTime(2006, 01, 03), to_date=DateTime(2006, 01, 03),
section_uid=self.mirror_section.getUid())), 1) section_uid=self.mirror_section.getUid())), 1)
def test_BrainDateTimeZone(self): def test_BrainDateTimeZone(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
...@@ -1765,7 +1765,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1765,7 +1765,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self._makeMovement(quantity=100, self._makeMovement(quantity=100,
start_date=date - 1, start_date=date - 1,
stop_date=date) stop_date=date)
movement_date_list = [ x.date for x in getMovementHistoryList( movement_date_list = [ x.date for x in getMovementHistoryList(
section_uid=self.section.getUid(), section_uid=self.section.getUid(),
sort_on=(('stock.date', 'ascending'),)) ] sort_on=(('stock.date', 'ascending'),)) ]
...@@ -1787,7 +1787,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1787,7 +1787,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
self._makeMovement(quantity=1, title='First') self._makeMovement(quantity=1, title='First')
self._makeMovement(quantity=2, title='Second') self._makeMovement(quantity=2, title='Second')
self.assertEquals(['First', 'Second'], [ x.getObject().getTitle() for x in self.assertEquals(['First', 'Second'], [ x.getObject().getTitle() for x in
getMovementHistoryList(section_uid=self.section.getUid(), getMovementHistoryList(section_uid=self.section.getUid(),
sort_on=(('title', 'ascending'),)) ]) sort_on=(('title', 'ascending'),)) ])
...@@ -1801,7 +1801,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1801,7 +1801,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self._makeMovement(quantity=1) self._makeMovement(quantity=1)
self.assertEquals(3, len(getMovementHistoryList(limit=3))) self.assertEquals(3, len(getMovementHistoryList(limit=3)))
self.assertEquals(4, len(getMovementHistoryList(limit=(1, 4)))) self.assertEquals(4, len(getMovementHistoryList(limit=(1, 4))))
def test_SimulationState(self): def test_SimulationState(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
self._makeMovement(quantity=2, simulation_state="confirmed") self._makeMovement(quantity=2, simulation_state="confirmed")
...@@ -1812,7 +1812,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1812,7 +1812,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
section_uid=self.section.getUid()) section_uid=self.section.getUid())
self.assertEquals(len(movement_history_list), 1) self.assertEquals(len(movement_history_list), 1)
self.assertEquals(movement_history_list[0].total_quantity, 2) self.assertEquals(movement_history_list[0].total_quantity, 2)
movement_history_list = getMovementHistoryList( movement_history_list = getMovementHistoryList(
simulation_state=["confirmed", "planned"], simulation_state=["confirmed", "planned"],
section_uid=self.section.getUid()) section_uid=self.section.getUid())
...@@ -1827,7 +1827,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1827,7 +1827,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
movement_history_list = getMovementHistoryList( movement_history_list = getMovementHistoryList(
section_uid=self.section.getUid()) section_uid=self.section.getUid())
self.assertEquals(2, len(movement_history_list)) self.assertEquals(2, len(movement_history_list))
def test_OmitSimulation(self): def test_OmitSimulation(self):
"""Test omit_simulation argument to getMovementHistoryList. """Test omit_simulation argument to getMovementHistoryList.
""" """
...@@ -1876,7 +1876,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1876,7 +1876,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self.assertEquals(running_total_quantity, brain.running_total_quantity) self.assertEquals(running_total_quantity, brain.running_total_quantity)
self.assertEquals(date, brain.date) self.assertEquals(date, brain.date)
self.assertEquals(quantity, brain.quantity) self.assertEquals(quantity, brain.quantity)
def test_RunningTotalPrice(self): def test_RunningTotalPrice(self):
"""Test that a running_total_price attribute is set on brains """Test that a running_total_price attribute is set on brains
""" """
...@@ -1923,7 +1923,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1923,7 +1923,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self.assertEquals(running_total_quantity, brain.running_total_quantity) self.assertEquals(running_total_quantity, brain.running_total_quantity)
running_total_price += quantity * quantity # we've set price=quantity running_total_price += quantity * quantity # we've set price=quantity
self.assertEquals(running_total_price, brain.running_total_price) self.assertEquals(running_total_price, brain.running_total_price)
def testRunningQuantityWithQuantity0(self): def testRunningQuantityWithQuantity0(self):
# a 0 quantity should not be a problem for running total price # a 0 quantity should not be a problem for running total price
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
...@@ -1951,7 +1951,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1951,7 +1951,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
stop_date=date+1, stop_date=date+1,
source_value=self.node, source_value=self.node,
destination_value=self.node ) destination_value=self.node )
mvt_history_list = getMovementHistoryList( mvt_history_list = getMovementHistoryList(
node_uid=self.node.getUid(),) node_uid=self.node.getUid(),)
self.assertEquals(2, len(mvt_history_list)) self.assertEquals(2, len(mvt_history_list))
...@@ -1967,7 +1967,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1967,7 +1967,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
node_uid=self.node.getUid(),) node_uid=self.node.getUid(),)
self.assertEquals(2, len(mvt_history_list)) self.assertEquals(2, len(mvt_history_list))
self.assertEquals(0, sum([r.total_quantity for r in mvt_history_list])) self.assertEquals(0, sum([r.total_quantity for r in mvt_history_list]))
def testSameNodeSameDatesSameSections(self): def testSameNodeSameDatesSameSections(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
mvt = self._makeMovement( quantity=2, mvt = self._makeMovement( quantity=2,
...@@ -1985,7 +1985,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1985,7 +1985,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
section_uid=self.section.getUid()) section_uid=self.section.getUid())
self.assertEquals(2, len(mvt_history_list)) self.assertEquals(2, len(mvt_history_list))
self.assertEquals(0, sum([r.total_quantity for r in mvt_history_list])) self.assertEquals(0, sum([r.total_quantity for r in mvt_history_list]))
def testPrecision(self): def testPrecision(self):
# getMovementHistoryList supports a precision= argument to specify the # getMovementHistoryList supports a precision= argument to specify the
# precision to round # precision to round
...@@ -1999,7 +1999,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -1999,7 +1999,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
self.assertEquals(0.12, mvt_history_list[0].running_total_price) self.assertEquals(0.12, mvt_history_list[0].running_total_price)
self.assertEquals(0.12, mvt_history_list[0].total_quantity) self.assertEquals(0.12, mvt_history_list[0].total_quantity)
self.assertEquals(0.12, mvt_history_list[0].total_price) self.assertEquals(0.12, mvt_history_list[0].total_price)
mvt_history_list = getMovementHistoryList( mvt_history_list = getMovementHistoryList(
precision=3, precision=3,
node_uid=self.node.getUid()) node_uid=self.node.getUid())
...@@ -2046,7 +2046,7 @@ class TestMovementHistoryList(InventoryAPITestCase): ...@@ -2046,7 +2046,7 @@ class TestMovementHistoryList(InventoryAPITestCase):
node_uid=self.node.getUid(), node_uid=self.node.getUid(),
omit_input=1, omit_input=1,
omit_output=1))) omit_output=1)))
def test_OmitAssetIncreaseDecrease(self): def test_OmitAssetIncreaseDecrease(self):
getMovementHistoryList = self.getSimulationTool().getMovementHistoryList getMovementHistoryList = self.getSimulationTool().getMovementHistoryList
m1 = self._makeMovement(quantity=1, price=-1) m1 = self._makeMovement(quantity=1, price=-1)
...@@ -2268,7 +2268,7 @@ class TestTrackingList(InventoryAPITestCase): ...@@ -2268,7 +2268,7 @@ class TestTrackingList(InventoryAPITestCase):
item_uid = self.item.getUid() item_uid = self.item.getUid()
other_item_uid = self.other_item.getUid() other_item_uid = self.other_item.getUid()
node_uid = self.node.getUid() node_uid = self.node.getUid()
self.assertEquals(len(getTrackingList(node_uid=node_uid, self.assertEquals(len(getTrackingList(node_uid=node_uid,
at_date=start_date)),0) at_date=start_date)),0)
makeMovement(aggregate=self.item) makeMovement(aggregate=self.item)
result = getTrackingList(node_uid=node_uid,at_date=start_date) result = getTrackingList(node_uid=node_uid,at_date=start_date)
...@@ -2292,7 +2292,7 @@ class TestTrackingList(InventoryAPITestCase): ...@@ -2292,7 +2292,7 @@ class TestTrackingList(InventoryAPITestCase):
item_uid = self.item.getUid() item_uid = self.item.getUid()
other_item_uid = self.other_item.getUid() other_item_uid = self.other_item.getUid()
node_uid = self.node.getUid() node_uid = self.node.getUid()
self.assertEquals(len(getTrackingList(node_uid=node_uid, self.assertEquals(len(getTrackingList(node_uid=node_uid,
at_date=start_date)),0) at_date=start_date)),0)
makeMovement(aggregate_list=[self.item.getRelativeUrl(), makeMovement(aggregate_list=[self.item.getRelativeUrl(),
self.other_item.getRelativeUrl()]) self.other_item.getRelativeUrl()])
...@@ -2351,133 +2351,62 @@ class TestTrackingList(InventoryAPITestCase): ...@@ -2351,133 +2351,62 @@ class TestTrackingList(InventoryAPITestCase):
'%s=now - %i, aggregate should be at node %i but is at node %i' % \ '%s=now - %i, aggregate should be at node %i but is at node %i' % \
(param_id, now - date, node_uid_to_node_number[location_uid], node_uid_to_node_number[uid_list[0]])) (param_id, now - date, node_uid_to_node_number[location_uid], node_uid_to_node_number[uid_list[0]]))
class TestInventoryDocument(InventoryAPITestCase): class TestInventoryCacheTable(InventoryAPITestCase):
""" Test impact of creating full inventories of stock points on inventory """ Test impact of creating cache entries into inventory_cache table
lookup. This is an optimisation to regular inventory system to avoid This is an optimisation on stock results to avoid reading all stock entries
reading all stock entries since a node/section/payment is used when for the same request.
gathering its amounts of resources.
Cache description:
A cache entry is defined by:
- a request identifier (implementation detail: MD5 of query source code)
- the date of first possible stock row *excluded* from cache entry (even if
there is no row at that exact date)
- cache data (implementation detail: pickle of a dict containing selected
properties of a query result)
Cache filling:
Cache might be filled by any request with either an at_date or a to_date,
and without a from_date. Cache entry date is chosen as "{at,to}_date -
cache_lag / 2".
No cache entry is created if a cache entry newer than "{at,to}_date -
cache_lag" exists.
Cache flush:
Cache is flushed by any modification (actually triggered on indexation or
de-indexation) on a stock-indexed document (aka movement). Only cache
entries with a date greater than any modified stock line are dropped.
""" """
def _createAutomaticInventoryAtDate(self, date, override_inventory=None, def afterSetUp(self):
full_inventory=False): InventoryAPITestCase.afterSetUp(self)
""" self.CACHE_LAG = cache_lag = self.getSimulationTool().getInventoryCacheLag()
getInventoryList is tested to work in another unit test. min_lag = cache_lag / 2
If full_inventory is false, only inventoriate the first resource self.NOW = now = DateTime(DateTime().strftime("%Y-%m-%d %H:%M:%S UTC"))
found. self.CACHE_DATE = cache_date = now - min_lag
""" self.LAST_CACHED_MOVEMENT_DATE = last_cached_movement_date = \
self.tic() # Tic so that grabbed inventory is up to date. cache_date - MYSQL_MIN_DATETIME_RESOLUTION
getInventoryList = self.getSimulationTool().getInventoryList # First movement, won't be into cache
portal = self.getPortal() self.INVENTORY_DATE_3 = INVENTORY_DATE_3 = now - 10
inventory_module = portal.getDefaultModule(portal_type='Inventory')
inventory = inventory_module.newContent(portal_type='Inventory')
inventory.edit(destination_value=self.node,
destination_section_value=self.section,
start_date=date,
full_inventory=full_inventory)
inventory_list = getInventoryList(node_uid=self.node.getUid(),
at_date=date,
omit_output=1)
if full_inventory:
inventory_list = [inventory_list[0]]
# TODO: Define a second resource which will only be present in full
# inventories. This will allow testing getInventoryList.
#else:
# inventory_list.append({'resource_relative_url': '','total_quantity': 50,'variation_text': ''})
for inventory_line in inventory_list:
line = inventory.newContent(portal_type='Inventory Line')
if override_inventory is None:
total_quantity = inventory_line['total_quantity']
else:
total_quantity = override_inventory
line.edit(resource=inventory_line['resource_relative_url'],
inventory=total_quantity,
variation_text=inventory_line['variation_text'])
# TODO: pass more properties through from calculated inventory to
# inventory lines if needed.
inventory.deliver()
return inventory
def _populateInventoryModule(self):
"""
Create 3 inventories:
Type Deviation Date (see stepCreateInitialMovements)
- partial 1000
- full 10000
- full 100000
"""
self.BASE_QUANTITY = BASE_QUANTITY = 1
# TODO: It would be better to strip numbers below seconds instead of below
# days.
self.MAX_DATE = MAX_DATE = DateTime(DateTime().Date()) - 1
self.DUPLICATE_INVENTORY_DATE = MAX_DATE - 8 # Newest
self.INVENTORY_DATE_3 = INVENTORY_DATE_3 = MAX_DATE - 10 # Newest
self.INVENTORY_QUANTITY_3 = INVENTORY_QUANTITY_3 = 100000 self.INVENTORY_QUANTITY_3 = INVENTORY_QUANTITY_3 = 100000
self.INVENTORY_DATE_2 = INVENTORY_DATE_2 = INVENTORY_DATE_3 - 10 # Second movement, won't be into cache, just at the limit of cache min_lag
self.INVENTORY_DATE_2 = INVENTORY_DATE_2 = cache_date
self.INVENTORY_QUANTITY_2 = INVENTORY_QUANTITY_2 = 10000 self.INVENTORY_QUANTITY_2 = INVENTORY_QUANTITY_2 = 10000
self.INVENTORY_DATE_1 = INVENTORY_DATE_1 = INVENTORY_DATE_2 - 10 # Oldest # Next will be stored as cache result after first getInventory
self.INVENTORY_DATE_1 = INVENTORY_DATE_1 = last_cached_movement_date
self.INVENTORY_QUANTITY_1 = INVENTORY_QUANTITY_1 = 1000 self.INVENTORY_QUANTITY_1 = INVENTORY_QUANTITY_1 = 1000
# Create movements
# "actual" quantities are the quantities which will end up in the stock self._makeMovement(
# table. quantity=INVENTORY_QUANTITY_1,
self.ACTUAL_INVENTORY_QUANTITY_1 = INVENTORY_QUANTITY_1 - \ start_date=INVENTORY_DATE_1,
BASE_QUANTITY simulation_state='delivered',
self.ACTUAL_INVENTORY_QUANTITY_2 = INVENTORY_QUANTITY_2 - \ )
(self.INVENTORY_QUANTITY_1 + BASE_QUANTITY) self._makeMovement(
self.ACTUAL_INVENTORY_QUANTITY_3 = INVENTORY_QUANTITY_3 - \ quantity=INVENTORY_QUANTITY_2,
(self.INVENTORY_QUANTITY_2 + BASE_QUANTITY) start_date=INVENTORY_DATE_2,
simulation_state='delivered',
self.movement_uid_list = movement_uid_list = [] )
# Initial movement of 1 self._makeMovement(
movement = self._makeMovement(quantity=BASE_QUANTITY, quantity=INVENTORY_QUANTITY_3,
start_date=INVENTORY_DATE_1 - 1, start_date=INVENTORY_DATE_3,
simulation_state='delivered') simulation_state='delivered',
movement_uid_list.append(movement.getUid()) )
# First (partial) inventory of 1 000
partial_inventory = self._createAutomaticInventoryAtDate(
date=INVENTORY_DATE_1, override_inventory=INVENTORY_QUANTITY_1)
# Second movement of 1
movement = self._makeMovement(quantity=BASE_QUANTITY,
start_date=INVENTORY_DATE_2 - 1,
simulation_state='delivered')
movement_uid_list.append(movement.getUid())
# Second (full) inventory of 10 000
self._createAutomaticInventoryAtDate(date=INVENTORY_DATE_2,
override_inventory=INVENTORY_QUANTITY_2,
full_inventory=True)
# Third movement of 1
movement = self._makeMovement(quantity=BASE_QUANTITY,
start_date=INVENTORY_DATE_3 - 1,
simulation_state='delivered')
movement_uid_list.append(movement.getUid())
# Third (full) inventory of 100 000
self._createAutomaticInventoryAtDate(date=INVENTORY_DATE_3,
override_inventory=INVENTORY_QUANTITY_3,
full_inventory=True)
# Fourth movement of 1
movement = self._makeMovement(quantity=BASE_QUANTITY,
start_date=INVENTORY_DATE_3 + 1,
simulation_state='delivered')
movement_uid_list.append(movement.getUid())
self.tic()
manage_test = self.getPortal().erp5_sql_transactionless_connection.manage_test
def executeSQL(query):
manage_test("BEGIN\x00%s\x00COMMIT" % (query, ))
# Make stock table inconsistent with inventory_stock to make sure
# inventory_stock is actually tested.
executeSQL("UPDATE stock SET quantity=quantity*2 WHERE uid IN (%s)" %
(', '.join([str(x) for x in movement_uid_list]), ))
self.BASE_QUANTITY *= 2
# Make inventory_stock table inconsistent with stock to make sure
# inventory_stock is actually not used when checking that partial
# inventory is not taken into account.
executeSQL("UPDATE inventory_stock SET quantity=quantity*2 WHERE "\
"uid IN (%s)" % (', '.join([str(x.getUid()) for x in \
partial_inventory.objectValues()]),
))
def afterSetUp(self):
InventoryAPITestCase.afterSetUp(self)
self._populateInventoryModule()
simulation_tool = self.getSimulationTool() simulation_tool = self.getSimulationTool()
self.getInventory = simulation_tool.getInventory self.getInventory = simulation_tool.getInventory
self.getInventoryList = simulation_tool.getInventoryList self.getInventoryList = simulation_tool.getInventoryList
...@@ -2537,586 +2466,458 @@ class TestInventoryDocument(InventoryAPITestCase): ...@@ -2537,586 +2466,458 @@ class TestInventoryDocument(InventoryAPITestCase):
# Leads to rasing exception instead of calling self.assert[...] method. # Leads to rasing exception instead of calling self.assert[...] method.
if not success: if not success:
if ordered_check: if ordered_check:
raise AssertionError, 'Line %r do not match %r' % \ raise AssertionError, 'Line %r\ndo not match\n %r' % \
(inventory_list[inventory_position], (inventory_list[inventory_position],
criterion_dict) criterion_dict)
else: else:
raise AssertionError, 'No line in %r match %r' % \ raise AssertionError, 'No line in %r\n match\n %r' % \
(inventory_list, criterion_dict) (inventory_list, criterion_dict)
# Check all expected lines have been found. # Check all expected lines have been found.
self.assertFalse(inventory_list) self.assertFalse(inventory_list)
def _fillCache(self, inventory_list=False):
"""
Calling getInventoryXXX will fill the cache for us
"""
if inventory_list:
result_list = self.getInventoryList(node_uid=self.node_uid, to_date=self.NOW)
result = 0
for line in result_list:
result += line.quantity
else:
result = self.getInventory(node_uid=self.node_uid, to_date=self.NOW)
self.assertEqual(result, self.INVENTORY_QUANTITY_1 + \
self.INVENTORY_QUANTITY_2 + self.INVENTORY_QUANTITY_3)
return result
def doubleStockValue(self):
"""
Make stock table inconsistent so that we can check that
optimisation is well used
"""
# in the test, this will always be the date at which an entry is put into
# inventory_cache
self.getPortalObject().erp5_sql_transactionless_connection.manage_test(
"BEGIN\0"
"UPDATE stock SET quantity=quantity*2 WHERE date < '%s'\0"
"COMMIT" % ((self.CACHE_DATE).ISO(), ))
self.commit()
def assertInventoryEquals(self, value, inventory_kw): def assertInventoryEquals(self, value, inventory_kw):
""" """
Check that optimised getInventory call is equal to given value Check that optimised getInventory call is equal to given value
and that unoptimised call is *not* equal to thi value. and that unoptimised call is *not* equal to thi value.
""" """
# Make stock table inconsistent to be sure it uses cache
self.doubleStockValue()
# Check it use cache
self.assertEquals(value, self.getInventory(**inventory_kw)) self.assertEquals(value, self.getInventory(**inventory_kw))
self.assertNotEquals(value, self.assertNotEquals(value,
self.getInventory(optimisation__=False, self.getInventory(optimisation__=False,
**inventory_kw)) **inventory_kw))
def setUpDefaultInventoryCalculationList(self): def test_01_CurrentInventory(self):
createZODBPythonScript(self.portal.portal_skins.custom,
'Inventory_getDefaultInventoryCalculationList', '',
'''return ({
'inventory_params':{
'section_uid':context.getDestinationSectionUid(),
'node_uid':context.getDestinationUid(),
'group_by_variation':1,
'group_by_resource':1},
'list_method':'getMovementList',
'first_level':({'key':'resource_relative_url',
'getter':'getResource',
'setter':('appendToCategoryList', 'resource')},
{'key':'variation_text',
'getter':'getVariationText',
'setter':'splitAndExtendToCategoryList'},
),
},)
''')
self.commit()
def clearAllInventoryAndSetUpTwoInventory(self):
self.portal.inventory_module.manage_delObjects(list(self.portal.inventory_module.objectIds()))
self.folder.manage_delObjects(list(self.folder.objectIds()))
self.resource.setProductLine('level1/level2')
self.other_resource.setProductLine('anotherlevel')
self.section.setGroup('level1/level2')
self.other_section.setGroup('anotherlevel')
self.node.setRegion('level1')
self.other_node.setGroup('anotherlevel')
full_inventory = self.portal.inventory_module.newContent(portal_type='Inventory')
full_inventory.edit(destination_section_value=self.section,
destination_value=self.node,
full_inventory=1,
start_date=DateTime('2012/05/18 00:00:00 GMT+9'))
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.resource)
line.setQuantity(1)
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.other_resource)
line.setQuantity(10)
full_inventory.deliver()
full_inventory = self.portal.inventory_module.newContent(portal_type='Inventory')
full_inventory.edit(destination_section_value=self.other_section,
destination_value=self.node,
full_inventory=1,
start_date=DateTime('2012/05/18 00:00:00 GMT+9'))
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.resource)
line.setQuantity(100)
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.other_resource)
line.setQuantity(1000)
full_inventory.deliver()
full_inventory = self.portal.inventory_module.newContent(portal_type='Inventory')
full_inventory.edit(destination_section_value=self.section,
destination_value=self.other_node,
full_inventory=1,
start_date=DateTime('2012/05/18 00:00:00 GMT+9'))
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.resource)
line.setQuantity(10000)
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.other_resource)
line.setQuantity(100000)
full_inventory.deliver()
full_inventory = self.portal.inventory_module.newContent(portal_type='Inventory')
full_inventory.edit(destination_section_value=self.other_section,
destination_value=self.other_node,
full_inventory=1,
start_date=DateTime('2012/05/18 00:00:00 GMT+9'))
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.resource)
line.setQuantity(1000000)
line = full_inventory.newContent(portal_type='Inventory Line')
line.setResourceValue(self.other_resource)
line.setQuantity(10000000)
full_inventory.deliver()
self.commit()
self.tic()
def test_01_CurrentInventoryWithFullInventory(self):
""" """
Check that inventory optimisation is executed when querying current Check that optimisation is executed when querying current
amount (there is a usable full inventory which is the latest). amount
""" """
self.assertInventoryEquals(value=self.INVENTORY_QUANTITY_3 + \ self.assertInventoryEquals(
self.BASE_QUANTITY, self._fillCache(),
inventory_kw={'node_uid': self.node_uid}) inventory_kw={
'node_uid': self.node_uid,
def test_02_InventoryAtLatestFullInventoryDate(self): 'to_date': self.NOW,
}
)
def test_02_InventoryAtCacheDate(self):
""" """
Check that inventory optimisation is executed when querying an amount Check that optimisation is executed when querying an amount
at the exact time of latest usable full inventory. at the exact time of the cache of result
""" """
self.assertInventoryEquals(value=self.INVENTORY_QUANTITY_3, self._fillCache()
inventory_kw={'node_uid': self.node_uid, # We got results from cache + results from stock
'at_date': self.INVENTORY_DATE_3}) self.assertInventoryEquals(
self.INVENTORY_QUANTITY_2 + self.INVENTORY_QUANTITY_1,
def test_03_InventoryAtEarlierFullInventoryDate(self): inventory_kw={
'node_uid': self.node_uid,
'at_date': self.CACHE_DATE,
},
)
def test_03_InventoryToCacheDate(self):
""" """
Check that inventory optimisation is executed when querying past Check that optimisation is executed when querying an amount
amount (there is a usable full inventory which is not the latest). that will only take into account cache data.
""" """
self.assertInventoryEquals(value=self.INVENTORY_QUANTITY_2 + \ self._fillCache()
self.BASE_QUANTITY, # We got only results from cache
inventory_kw={'node_uid': self.node_uid, self.assertInventoryEquals(
'at_date': self.INVENTORY_DATE_3 - \ self.INVENTORY_QUANTITY_1,
1}) inventory_kw={
'node_uid': self.node_uid,
def test_04_InventoryBeforeFullInventoryAfterPartialInventory(self): 'to_date': self.CACHE_DATE,
},
)
def test_04_InventoryList(self):
""" """
Check that optimisation is not executed when querying past amount Check that optimisation is executed when querying current
with no usable full inventory. amount list
If optimisation was executed,
self.INVENTORY_QUANTITY_1 * 2 + self.BASE_QUANTITY * 2
would be found.
""" """
self.assertEquals(self.ACTUAL_INVENTORY_QUANTITY_1 + \ self._fillCache(True)
self.BASE_QUANTITY * 2, # Check we got all results
self.getInventory(node_uid=self.node_uid, self._checkInventoryList(
at_date=self.INVENTORY_DATE_2 - 1)) self.getInventoryList(node_uid=self.node_uid, to_date=self.NOW),
[{
def test_05_InventoryListWithFullInventory(self): 'date': self.INVENTORY_DATE_3,
'inventory': self.INVENTORY_QUANTITY_3,
'node_uid': self.node_uid,
}, {
'date': self.INVENTORY_DATE_2,
'inventory': self.INVENTORY_QUANTITY_2,
'node_uid': self.node_uid,
}, {
'date': self.INVENTORY_DATE_1,
'inventory': self.INVENTORY_QUANTITY_1,
'node_uid': self.node_uid,
}],
)
def test_05_InventoryListAtCacheDate(self):
""" """
Check that inventory optimisation is executed when querying current Check that optimisation is executed when querying an amount list
amount list (there is a usable full inventory which is the latest). at the exact time of the cache of result
""" """
inventory = self.getInventoryList(node_uid=self.node_uid) self._fillCache(True)
reference_inventory = [ # We got results from cache + results from stock
{'date': self.INVENTORY_DATE_3, self._checkInventoryList(
'inventory': self.INVENTORY_QUANTITY_3, self.getInventoryList(node_uid=self.node_uid, at_date=self.CACHE_DATE),
'node_uid': self.node_uid}, [{
{'date': self.INVENTORY_DATE_3 + 1, 'date': self.INVENTORY_DATE_1,
'inventory': self.BASE_QUANTITY, 'inventory': self.INVENTORY_QUANTITY_1,
'node_uid': self.node_uid} 'node_uid': self.node_uid,
] }, {
self._checkInventoryList(inventory, reference_inventory) 'date': self.INVENTORY_DATE_2,
'inventory': self.INVENTORY_QUANTITY_2,
def test_06_InventoryListAtLatestFullInventoryDate(self): 'node_uid': self.node_uid,
}],
)
def test_06_InventoryListToCacheDate(self):
""" """
Check that inventory optimisation is executed when querying past Check that optimisation is executed when querying an amount list
amount list (there is a usable full inventory which is not the latest). that will only take into account cache data.
""" """
inventory = self.getInventoryList(node_uid=self.node_uid, self._fillCache(True)
at_date=self.INVENTORY_DATE_3) # We got only results from cache
reference_inventory = [ self._checkInventoryList(
{'date': self.INVENTORY_DATE_3, self.getInventoryList(node_uid=self.node_uid, to_date=self.CACHE_DATE),
'inventory': self.INVENTORY_QUANTITY_3, [{
'node_uid': self.node_uid} 'date': self.INVENTORY_DATE_1,
] 'inventory': self.INVENTORY_QUANTITY_1,
self._checkInventoryList(inventory, reference_inventory) 'node_uid': self.node_uid,
}],
def test_07_InventoryListAtEarlierFullInventoryDate(self): )
"""
Check that inventory optimisation is executed when querying past def test_07_InventoryListGroupedByResource(self):
amount list (there is a usable full inventory which is not the latest).
"""
inventory = self.getInventoryList(node_uid=self.node_uid,
at_date=self.INVENTORY_DATE_3 - 1)
reference_inventory = [
{'date': self.INVENTORY_DATE_2,
'inventory': self.INVENTORY_QUANTITY_2,
'node_uid': self.node_uid},
{'date': self.INVENTORY_DATE_3 - 1,
'inventory': self.BASE_QUANTITY,
'node_uid': self.node_uid}
]
self._checkInventoryList(inventory, reference_inventory)
def test_08_InventoryListBeforeFullInventoryAfterPartialInventory(self):
"""
Check that optimisation is not executed when querying past amount list
with no usable full inventory.
"""
inventory = self.getInventoryList(node_uid=self.node_uid,
at_date=self.INVENTORY_DATE_2 - 1)
reference_inventory = [
{'date': self.INVENTORY_DATE_1 - 1,
'inventory': self.BASE_QUANTITY,
'node_uid': self.node_uid},
{'date': self.INVENTORY_DATE_1,
'inventory': self.ACTUAL_INVENTORY_QUANTITY_1,
'node_uid': self.node_uid},
{'date': self.INVENTORY_DATE_2 - 1,
'inventory': self.BASE_QUANTITY,
'node_uid': self.node_uid}
]
self._checkInventoryList(inventory, reference_inventory)
def test_09_InventoryListGroupedByResource(self):
""" """
Group inventory list by resource explicitely, used inventory is the Check that optimisation is executed when grouping inventory list
latest. by resource explicitely
""" """
inventory = self.getInventoryList(node_uid=self.node_uid, inventory_kw = {
group_by_resource=1) 'node_uid': self.node_uid,
reference_inventory = [ 'to_date': self.NOW,
{'inventory': self.INVENTORY_QUANTITY_3 + self.BASE_QUANTITY, 'group_by_resource': 1,
'resource_uid': self.resource.getUid(), }
'node_uid': self.node_uid} # Fill cache
] self.getInventoryList(**inventory_kw)
self._checkInventoryList(inventory, reference_inventory) # Check we got all results
self._checkInventoryList(
def test_10_InventoryListGroupedByResourceBeforeLatestFullInventoryDate(self): self.getInventoryList(**inventory_kw),
[{
'inventory': self.INVENTORY_QUANTITY_3 + self.INVENTORY_QUANTITY_2 + \
self.INVENTORY_QUANTITY_1,
'resource_uid': self.resource.getUid(),
'node_uid': self.node_uid,
}],
)
def test_08_InventoryListGroupedByResourceAtCacheDate(self):
""" """
Group inventory list by resource explicitely, used inventory is not the Check that optimisation is executed when grouping inventory list
latest. by resource explicitely
""" """
inventory = self.getInventoryList(node_uid=self.node_uid, # Fill cache
group_by_resource=1, self.getInventoryList(node_uid=self.node_uid, to_date=self.NOW,
at_date=self.INVENTORY_DATE_3 - 1) group_by_resource=1)
reference_inventory = [ # We got results from cache + results from stock
{'inventory': self.INVENTORY_QUANTITY_2 + self.BASE_QUANTITY, self._checkInventoryList(
'resource_uid': self.resource.getUid(), self.getInventoryList(node_uid=self.node_uid, at_date=self.CACHE_DATE,
'node_uid': self.node_uid} group_by_resource=1),
] [{
self._checkInventoryList(inventory, reference_inventory) 'inventory': self.INVENTORY_QUANTITY_2 + self.INVENTORY_QUANTITY_1,
'resource_uid': self.resource.getUid(),
def test_11_InventoryListAroundLatestInventoryDate(self): 'node_uid': self.node_uid,
}],
)
def test_09_InventoryListGroupByResourceToCacheDate(self):
""" """
Test getInventoryList with a min and a max date around latest full Check that optimisation is executed when grouping inventory list
inventory. A full inventory is used and is not the latest. by resource explicitely
""" """
inventory = self.getInventoryList(node_uid=self.node_uid, self.getInventoryList(node_uid=self.node_uid, to_date=self.NOW, group_by_resource=1)
from_date=self.INVENTORY_DATE_3 - 1, # We got only results from cache
at_date=self.INVENTORY_DATE_3 + 1) self._checkInventoryList(
reference_inventory = [ self.getInventoryList(node_uid=self.node_uid, to_date=self.CACHE_DATE,
{'inventory': self.BASE_QUANTITY, group_by_resource=1),
'resource_uid': self.resource.getUid(), [{
'node_uid': self.node_uid, 'inventory': self.INVENTORY_QUANTITY_1,
'date': self.INVENTORY_DATE_3 - 1}, 'resource_uid': self.resource.getUid(),
{'inventory': self.ACTUAL_INVENTORY_QUANTITY_3, 'node_uid': self.node_uid,
'resource_uid': self.resource.getUid(), }],
'node_uid': self.node_uid, )
'date': self.INVENTORY_DATE_3},
{'inventory': self.BASE_QUANTITY, def test_10_InventoryListWithOrderByDate(self):
'resource_uid': self.resource.getUid(),
'node_uid': self.node_uid,
'date': self.INVENTORY_DATE_3 + 1}
]
self._checkInventoryList(inventory, reference_inventory)
def test_12_InventoryListWithOrderByDate(self):
""" """
Test order_by is preserved by optimisation on date column. Test order_by is preserved by optimisation on date column.
Also sort on total_quantity column because there are inventory lines
which are on the same date but with distinct quantities.
""" """
inventory = self.getInventoryList(node_uid=self.node_uid, inventory_kw={
from_date=self.INVENTORY_DATE_3 - 1, 'node_uid': self.node_uid,
at_date=self.INVENTORY_DATE_3 + 1, 'to_date': self.NOW,
sort_on=(('date', 'ASC'), }
('total_quantity', 'DESC'))) sort_on = (('date', 'ASC'), ('total_quantity', 'DESC'))
reference_inventory = [ reversed_sort_on = (('date', 'DESC'), ('total_quantity', 'ASC'))
{'inventory': self.BASE_QUANTITY, reference_inventory = [{
'resource_uid': self.resource.getUid(), 'inventory': self.INVENTORY_QUANTITY_1,
'node_uid': self.node_uid, 'resource_uid': self.resource.getUid(),
'date': self.INVENTORY_DATE_3 - 1}, 'node_uid': self.node_uid,
{'inventory': self.ACTUAL_INVENTORY_QUANTITY_3, 'date': self.INVENTORY_DATE_1,
'resource_uid': self.resource.getUid(), }, {
'node_uid': self.node_uid, 'inventory': self.INVENTORY_QUANTITY_2,
'date': self.INVENTORY_DATE_3}, 'resource_uid': self.resource.getUid(),
{'inventory': self.BASE_QUANTITY, 'node_uid': self.node_uid,
'resource_uid': self.resource.getUid(), 'date': self.INVENTORY_DATE_2
'node_uid': self.node_uid, }, {
'date': self.INVENTORY_DATE_3 + 1} 'inventory': self.INVENTORY_QUANTITY_3,
] 'resource_uid': self.resource.getUid(),
self._checkInventoryList(inventory, reference_inventory, 'node_uid': self.node_uid,
ordered_check=True) 'date': self.INVENTORY_DATE_3,
inventory = self.getInventoryList(node_uid=self.node_uid, }]
from_date=self.INVENTORY_DATE_3 - 1, # Fill cache
at_date=self.INVENTORY_DATE_3 + 1, self.getInventoryList(sort_on=sort_on, **inventory_kw)
sort_on=(('date', 'DESC'), # Check it in fist order
('total_quantity', 'ASC'))) self._checkInventoryList(
self.getInventoryList(sort_on=sort_on, **inventory_kw),
reference_inventory,
ordered_check=True,
)
# Check it in reverse order
reference_inventory.reverse() reference_inventory.reverse()
self._checkInventoryList(inventory, reference_inventory, self._checkInventoryList(
ordered_check=True) self.getInventoryList(sort_on=reversed_sort_on, **inventory_kw),
reference_inventory,
ordered_check=True,
)
def test_13_InventoryAfterModificationInPast(self): def test_11_InventoryWithFromDate(self):
""" """
Test inventory after adding a new movement in past and reindex all inventory Test that getInventory called with from date does not generate cache entry
""" """
movement = self._makeMovement(quantity=self.BASE_QUANTITY*2, inventory_kw = {
start_date=self.INVENTORY_DATE_3 - 2, 'node_uid': self.node_uid,
simulation_state='delivered') 'to_date': self.NOW,
# reindex inventory module, although we modified table by hand 'from_date': self.NOW - self.CACHE_LAG - 10,
# everything must be consistent after reindexation }
inventory_module = self.getPortal().getDefaultModule(portal_type='Inventory') # Double stock value and check that we got same result
inventory_module.recursiveReindexObject() # with and without optimisation
self.tic() value = self.INVENTORY_QUANTITY_3 + self.INVENTORY_QUANTITY_2 + \
inventory_kw={'node_uid': self.node_uid, 2 * self.INVENTORY_QUANTITY_1
'at_date': self.INVENTORY_DATE_3} self.doubleStockValue()
value=self.INVENTORY_QUANTITY_3
# use optimisation
self.assertEquals(value, self.getInventory(**inventory_kw)) self.assertEquals(value, self.getInventory(**inventory_kw))
# without optimisation
self.assertEquals(value, self.assertEquals(value,
self.getInventory(optimisation__=False, self.getInventory(optimisation__=False,
**inventory_kw)) **inventory_kw))
def test_14_TwoInventoryWithSameDateAndResourceAndNode(self): def test_12_CheckCacheFlush(self):
""" """
It makes no sense to validate two inventories with same date, Test the cache is flushed when indexing a movement into stock
same resource, and same node. The calculation of inventories
will not work in such case. So here we test that a constraint
does not allow such things
""" """
portal = self.getPortal() inventory_kw = {
self._addPropertySheet('Inventory', 'InventoryConstraint') 'node_uid': self.node_uid,
try: 'to_date': self.NOW,
inventory_module = portal.getDefaultModule(portal_type='Inventory') }
inventory = inventory_module.newContent(portal_type='Inventory') # Fill cache and make stock inconsistent
date = self.DUPLICATE_INVENTORY_DATE value = self.getInventory(**inventory_kw)
inventory.edit(destination_value=self.node, self.doubleStockValue()
destination_section_value=self.section, # Create a movement after CACHE DATE
start_date=date) # Cache is cleared for all entry > movement date
inventory_line = inventory.newContent( # as at a cache date D, it contains results from stock
resource_value = self.resource, # for all line < D
quantity = 1) INVENTORY_QUANTITY_4 = 5000
self.workflow_tool = portal.portal_workflow INVENTORY_DATE_4 = self.CACHE_DATE
workflow_id = 'inventory_workflow' movement = self._makeMovement(
transition_id = 'deliver_action' quantity=INVENTORY_QUANTITY_4,
workflow_id= 'inventory_workflow' start_date=INVENTORY_DATE_4,
self.workflow_tool.doActionFor(inventory, transition_id, simulation_state='delivered',
wf_id=workflow_id) )
self.assertEquals('delivered', inventory.getSimulationState()) self.tic()
self.tic() # Optimisation must still be used
self.assertEquals(
# We should detect the previous inventory and fails value + INVENTORY_QUANTITY_4,
new_inventory = inventory.Base_createCloneDocument(batch_mode=1) self.getInventory(**inventory_kw),
self.assertRaises(ValidationFailed, self.workflow_tool.doActionFor, )
new_inventory, transition_id, wf_id=workflow_id) # Edit start date so that cache table is cleared
workflow_history = self.workflow_tool.getInfoFor(ob=new_inventory, movement.edit(start_date=self.LAST_CACHED_MOVEMENT_DATE)
name='history', wf_id=workflow_id) self.tic()
workflow_error_message = str(workflow_history[-1]['error_message']) self.assertEquals(
self.assertTrue(len(workflow_error_message)) value + INVENTORY_QUANTITY_4 + self.INVENTORY_QUANTITY_1,
self.assertTrue(len([x for x in workflow_error_message \ self.getInventory(**inventory_kw),
if x.find('There is already an inventory')])) )
self.doubleStockValue()
# Add a case in order to check a bug when the other inventory at the # Cache hit again
# same date does not change stock values self.assertEquals(
new_inventory = inventory.Base_createCloneDocument(batch_mode=1) value + INVENTORY_QUANTITY_4 + self.INVENTORY_QUANTITY_1,
new_inventory.setStartDate(self.DUPLICATE_INVENTORY_DATE + 1) self.getInventory(**inventory_kw),
self.workflow_tool.doActionFor(new_inventory, transition_id, )
wf_id=workflow_id) # Delete movement, so it gets unindexed
self.assertEquals('delivered', new_inventory.getSimulationState()) self.folder.manage_delObjects(ids=[movement.getId(), ])
self.tic() self.tic()
self.assertEquals(
new_inventory = new_inventory.Base_createCloneDocument(batch_mode=1) value + 3 * self.INVENTORY_QUANTITY_1,
self.assertRaises(ValidationFailed, self.workflow_tool.doActionFor, self.getInventory(**inventory_kw),
new_inventory, transition_id, wf_id=workflow_id) )
workflow_history = self.workflow_tool.getInfoFor(ob=new_inventory, self.doubleStockValue()
name='history', wf_id=workflow_id) # Cache hit again
workflow_error_message = str(workflow_history[-1]['error_message']) self.assertEquals(
self.assertTrue(len(workflow_error_message)) value + 3 * self.INVENTORY_QUANTITY_1,
self.assertTrue(len([x for x in workflow_error_message \ self.getInventory(**inventory_kw),
if x.find('There is already an inventory')])) )
finally:
# remove all property sheets we added to type informations def test_13_CacheCreatedFromCache(self):
ttool = self.getTypesTool()
for ti_name, psheet_list in self._added_property_sheets.iteritems():
ti = ttool.getTypeInfo(ti_name)
property_sheet_set = set(ti.getTypePropertySheetList())
property_sheet_set.difference_update(psheet_list)
ti._setTypePropertySheetList(list(property_sheet_set))
self.commit()
_aq_reset()
def test_15_InventoryAfterModificationInFuture(self):
""" """
Test inventory after adding a new movement in future Test that a new cache entry is created from previous cache entry if it exists
""" """
movement = self._makeMovement(quantity=self.BASE_QUANTITY*2, # Create an old movement
start_date=self.INVENTORY_DATE_3 + 2, INVENTORY_QUANTITY_4 = 100
simulation_state='delivered') INVENTORY_DATE_4 = self.NOW - 3 * self.CACHE_LAG
self.tic() movement = self._makeMovement(quantity=INVENTORY_QUANTITY_4,
start_date=INVENTORY_DATE_4,
simulation_state='delivered')
# Get inventory in past so that cache is filled
inventory_kw={'node_uid': self.node_uid,
"to_date" : self.NOW - 2 * self.CACHE_LAG,}
value = self.getInventory(**inventory_kw)
self.assertInventoryEquals(value, inventory_kw)
# Now compute wanted value manually as we screwed the stock table
# As we double every movement < CACHE_LAG/2, inventory_1 is doubled
# like inventory_4, but inventory_4 must be retrieved from cache with
# its initial value
wanted_value = 2 * self.INVENTORY_QUANTITY_1 + self.INVENTORY_QUANTITY_2 + \
self.INVENTORY_QUANTITY_3 + INVENTORY_QUANTITY_4
inventory_kw={'node_uid': self.node_uid,
"to_date" : self.NOW,}
value = self.getInventory(**inventory_kw)
self.assertEqual(value, wanted_value)
# Make sure it has filled a new cache
self.assertInventoryEquals(wanted_value, inventory_kw)
def getCurrentInventoryPathList(resource, **kw):
# the brain is not a zsqlbrain instance here, so it does not
# have getPath().
return [x.path for x in resource.getCurrentInventoryList(**kw)]
# use optimisation
self.assertEquals(True,movement.getPath() in
[x.path for x in self.resource.getInventoryList(
mirror_uid=self.mirror_node.getUid())])
# without optimisation
self.assertEquals(True,movement.getPath() in
[x.path for x in self.resource.getInventoryList(
optimisation__=False,
mirror_uid=self.mirror_node.getUid())])
@expectedFailure def test_14_CacheTableCreatedOnGetInventoryCall(self):
def test_MultipleSectionAndFullInventory(self):
"""Make sure that getInventoryList works in the situation which
two sections use the same node and one section has full inventory for
the node.
""" """
# In this test we do not need doucments made by afterSetUp. Check that getInventory does not fail it cache table does not exist
self.portal.inventory_module.manage_delObjects(list(self.portal.inventory_module.objectIds())) and that it create the table and add an entry in it
self.folder.manage_delObjects(list(self.folder.objectIds())) """
self.portal.SimulationTool_zDropInventoryCache()
self.commit() # Make sure it is dropped
self.assertRaises(ProgrammingError,
self.portal.SimulationTool_zTrimInventoryCacheFromDateOnCatalog,
date=DateTime())
# Check that src__ call still works
inventory_kw={
'node_uid': self.node_uid,
'to_date': self.NOW,
}
self.getInventory(src__=1, **inventory_kw)
# Table is still not created
self.assertRaises(ProgrammingError,
self.portal.SimulationTool_zTrimInventoryCacheFromDateOnCatalog,
date=DateTime())
# This call should not fail
# It will create table, fill it and check optimisation is used
self.assertInventoryEquals(
self._fillCache(),
inventory_kw=inventory_kw,
)
def test_15_CacheTableCreatedOnIndexation(self):
"""
Check that getInventory does not fail it cache table does not exist
and that it create the table and add an entry in it
"""
self.portal.SimulationTool_zDropInventoryCache()
# Make sure it is dropped
self.assertRaises(ProgrammingError,
self.portal.SimulationTool_zTrimInventoryCacheFromDateOnCatalog,
date=DateTime())
# Create a new movement, indexation should not fail
INVENTORY_QUANTITY_4 = 5000
INVENTORY_DATE_4 = self.CACHE_DATE
movement = self._makeMovement(
quantity=INVENTORY_QUANTITY_4,
start_date=INVENTORY_DATE_4,
simulation_state='delivered',
)
self.tic() self.tic()
createZODBPythonScript(self.portal.portal_skins.custom, # Optimisation must then be used
'Inventory_getDefaultInventoryCalculationList', '', inventory_kw={
'''return ({ 'node_uid': self.node_uid,
'inventory_params':{ 'to_date': self.NOW,
'section_uid':context.getDestinationSectionUid(), }
'node_uid':context.getDestinationUid(), value = self.getInventory(**inventory_kw)
'group_by_variation':1, self.assertInventoryEquals(
'group_by_resource':1}, value,
'list_method':'getMovementList', inventory_kw=inventory_kw,
'first_level':({'key':'resource_relative_url', )
'getter':'getResource',
'setter':('appendToCategoryList', 'resource')}, def test_16_CacheTableCreatedOnUnindexation(self):
{'key':'variation_text', """
'getter':'getVariationText', Check that getInventory does not fail it cache table does not exist
'setter':'splitAndExtendToCategoryList'}, and that it create the table and add an entry in it
), """
},) # Create a new movement
''') INVENTORY_QUANTITY_4 = 5000
INVENTORY_DATE_4 = self.CACHE_DATE
self.commit() movement = self._makeMovement(
quantity=INVENTORY_QUANTITY_4,
getCurrentInventoryList = self.getSimulationTool().getCurrentInventoryList start_date=INVENTORY_DATE_4,
simulation_state='delivered',
# Add movements for section )
self._makeMovement(source_section_value=None,
source_value=None,
destination_section_value=self.section,
destination_value=self.node,
start_date=DateTime('2012/07/18 00:00:00 GMT+9'),
simulation_state='delivered',
resource_value=self.resource,
quantity=1)
self._makeMovement(source_section_value=None,
source_value=None,
destination_section_value=self.section,
destination_value=self.node,
start_date=DateTime('2012/07/21 00:00:00 GMT+9'),
simulation_state='delivered',
resource_value=self.resource,
quantity=2)
# Add movemnets for other section
self._makeMovement(source_section_value=None,
source_value=None,
destination_section_value=self.other_section,
destination_value=self.node,
start_date=DateTime('2012/07/19 00:00:00 GMT+9'),
simulation_state='delivered',
resource_value=self.resource,
quantity=3)
self._makeMovement(source_section_value=None,
source_value=None,
destination_section_value=self.other_section,
destination_value=self.node,
start_date=DateTime('2012/07/20 00:00:00 GMT+9'),
simulation_state='delivered',
resource_value=self.resource,
quantity=4)
self.commit()
self.tic() self.tic()
# Check it also works on unindexation
# Check inventory self.portal.SimulationTool_zDropInventoryCache()
result = {} # Make sure it is dropped
for brain in getCurrentInventoryList(node_uid=self.node.getUid(), self.assertRaises(ProgrammingError,
group_by_resource=1, self.portal.SimulationTool_zTrimInventoryCacheFromDateOnCatalog,
group_by_node=1, date=DateTime())
group_by_section=1): # Delete movement
key = (brain.section_uid, brain.node_uid, brain.resource_uid) self.folder.manage_delObjects(ids=[movement.getId(), ])
if not key in result:
result[key] = 0
result[key] = result[key] + brain.inventory
self.assertEqual(result,
{(self.section.getUid(), self.node.getUid(), self.resource.getUid()):3,
(self.other_section.getUid(), self.node.getUid(), self.resource.getUid()):7})
# Add full inventory for section, not for other section
full_inventory1 = self.portal.inventory_module.newContent(portal_type='Inventory')
full_inventory1.edit(destination_section_value=self.section,
destination_value=self.node,
full_inventory=1,
start_date=DateTime('2012/07/20 00:00:00 GMT+9'))
line = full_inventory1.newContent(portal_type='Inventory Line')
line.setResourceValue(self.resource)
line.setQuantity(100)
full_inventory1.deliver()
self.commit()
self.tic() self.tic()
# This call must not fail as table has been created
self.portal.SimulationTool_zTrimInventoryCacheFromDateOnCatalog(date=DateTime())
# Check inventory again. This time, full inventory should change # This call should not fail
# section's inventory. It should not change other section's inventory. # It will create table, fill it and check optimisation is used
result = {} self.assertInventoryEquals(
for brain in getCurrentInventoryList(node_uid=self.node.getUid(), self._fillCache(),
group_by_resource=1, inventory_kw={
group_by_node=1, 'node_uid': self.node_uid,
group_by_section=1): 'to_date': self.NOW,
key = (brain.section_uid, brain.node_uid, brain.resource_uid) }
if not key in result: )
result[key] = 0
result[key] = result[key] + brain.inventory
self.assertEqual(result,
{(self.section.getUid(), self.node.getUid(), self.resource.getUid()):102,
(self.other_section.getUid(), self.node.getUid(), self.resource.getUid()):7})
@expectedFailure
def test_ResourceCategory(self):
"""Make sure that resource category works when full inventory exists."""
# In this test we do not need doucments made by afterSetUp.
self.setUpDefaultInventoryCalculationList()
self.clearAllInventoryAndSetUpTwoInventory()
self.assertEquals(1,
self.getInventory(section_uid=self.section.getUid(),
node_uid=self.node.getUid(),
resource_category='product_line/level1',
optimisation__=False))
self.assertEquals(1,
self.getInventory(section_uid=self.section.getUid(),
node_uid=self.node.getUid(),
resource_category='product_line/level1',
optimisation__=True))
@expectedFailure
def test_SectionCategory(self):
"""Make sure that section category works when full inventory exists."""
# In this test we do not need doucments made by afterSetUp.
self.clearAllInventoryAndSetUpTwoInventory()
self.assertEquals(11,
self.getInventory(node_uid=self.node.getUid(),
section_category='group/level1/level2',
optimisation__=False))
self.assertEquals(11,
self.getInventory(node_uid=self.node.getUid(),
section_category='group/level1/level2',
optimisation__=True))
@expectedFailure
def test_NodeCategory(self):
# In this test we do not need doucments made by afterSetUp.
self.clearAllInventoryAndSetUpTwoInventory()
self.assertEquals(11,
self.getInventory(section_uid=self.section.getUid(),
node_category='region/level1',
optimisation__=False))
self.assertEquals(11,
self.getInventory(section_uid=self.section.getUid(),
node_category='region/level1',
optimisation__=True))
class BaseTestUnitConversion(InventoryAPITestCase): class BaseTestUnitConversion(InventoryAPITestCase):
QUANTITY_UNIT_DICT = {} QUANTITY_UNIT_DICT = {}
...@@ -3543,10 +3344,10 @@ def test_suite(): ...@@ -3543,10 +3344,10 @@ def test_suite():
suite.addTest(unittest.makeSuite(TestInventoryStat)) suite.addTest(unittest.makeSuite(TestInventoryStat))
suite.addTest(unittest.makeSuite(TestNextNegativeInventoryDate)) suite.addTest(unittest.makeSuite(TestNextNegativeInventoryDate))
suite.addTest(unittest.makeSuite(TestTrackingList)) suite.addTest(unittest.makeSuite(TestTrackingList))
suite.addTest(unittest.makeSuite(TestInventoryDocument)) suite.addTest(unittest.makeSuite(TestInventoryCacheTable))
suite.addTest(unittest.makeSuite(TestUnitConversion)) suite.addTest(unittest.makeSuite(TestUnitConversion))
suite.addTest(unittest.makeSuite(TestUnitConversionDefinition)) suite.addTest(unittest.makeSuite(TestUnitConversionDefinition))
suite.addTest(unittest.makeSuite(TestUnitConversionBackwardCompatibility)) suite.addTest(unittest.makeSuite(TestUnitConversionBackwardCompatibility))
return suite return suite
# vim: foldmethod=marker
...@@ -1554,8 +1554,12 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor): ...@@ -1554,8 +1554,12 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
# Do a hot reindex in the reverse way, but this time a more # Do a hot reindex in the reverse way, but this time a more
# complicated hot reindex # complicated hot reindex
portal_catalog.manage_hotReindexAll(self.new_catalog_id, portal_catalog.manage_hotReindexAll(
self.original_catalog_id) source_sql_catalog_id=self.new_catalog_id,
destination_sql_catalog_id=self.original_catalog_id,
source_sql_connection_id_list=destination_sql_connection_id_list,
destination_sql_connection_id_list=source_sql_connection_id_list,
update_destination_sql_catalog=True)
self.commit() self.commit()
self.assertEquals(portal_catalog.getHotReindexingState(), self.assertEquals(portal_catalog.getHotReindexingState(),
HOT_REINDEXING_RECORDING_STATE) HOT_REINDEXING_RECORDING_STATE)
...@@ -1735,6 +1739,17 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor): ...@@ -1735,6 +1739,17 @@ class TestERP5Catalog(ERP5TypeTestCase, LogInterceptor):
# Check more cached methods of SQLCatalog by building SQLQuery # Check more cached methods of SQLCatalog by building SQLQuery
query = portal_catalog.getSQLCatalog().buildQuery(kw={'dummy.dummy_title': 'Foo'}) query = portal_catalog.getSQLCatalog().buildQuery(kw={'dummy.dummy_title': 'Foo'})
self.assertTrue(query.query_list) self.assertTrue(query.query_list)
# We need to reset SQL connections in skin folder's zsql methods
sql_connection_id_dict = {}
for destination_sql_connection_id, source_sql_connection_id in \
zip(destination_sql_connection_id_list,
source_sql_connection_id_list):
if source_sql_connection_id != destination_sql_connection_id:
sql_connection_id_dict[destination_sql_connection_id] = \
source_sql_connection_id
portal_catalog.changeSQLConnectionIds(
folder=portal.portal_skins,
sql_connection_id_dict = sql_connection_id_dict)
def test_47_Unrestricted(self, quiet=quiet, run=run_all_test): def test_47_Unrestricted(self, quiet=quiet, run=run_all_test):
"""test unrestricted search/count results. """test unrestricted search/count results.
......
...@@ -6,7 +6,9 @@ ...@@ -6,7 +6,9 @@
from Products.PythonScripts.Utility import allow_class from Products.PythonScripts.Utility import allow_class
from Products.CMFCore.WorkflowCore import WorkflowException from Products.CMFCore.WorkflowCore import WorkflowException
from MySQLdb import ProgrammingError
allow_class(ProgrammingError)
class DeferredCatalogError(Exception): class DeferredCatalogError(Exception):
...@@ -53,3 +55,4 @@ allow_class(WorkflowException) ...@@ -53,3 +55,4 @@ allow_class(WorkflowException)
allow_class(UnsupportedWorkflowMethod) allow_class(UnsupportedWorkflowMethod)
allow_class(TransformationRuleError) allow_class(TransformationRuleError)
allow_class(SimulationError) allow_class(SimulationError)
...@@ -14,7 +14,6 @@ import os ...@@ -14,7 +14,6 @@ import os
import random import random
import re import re
import socket import socket
import shutil
import sys import sys
import time import time
import traceback import traceback
...@@ -26,7 +25,6 @@ from glob import glob ...@@ -26,7 +25,6 @@ from glob import glob
from hashlib import md5 from hashlib import md5
from warnings import warn from warnings import warn
from ExtensionClass import pmc_init_of from ExtensionClass import pmc_init_of
from ZTUtils import make_query
from DateTime import DateTime from DateTime import DateTime
# XXX make sure that get_request works. # XXX make sure that get_request works.
...@@ -148,17 +146,17 @@ def _getConnectionStringDict(): ...@@ -148,17 +146,17 @@ def _getConnectionStringDict():
def _getConversionServerDict(): def _getConversionServerDict():
""" Returns a dict with hostname and port for Conversion Server (Oood) """ Returns a dict with hostname and port for Conversion Server (Oood)
""" """
conversion_server_hostname = os.environ.get('conversion_server_hostname', conversion_server_hostname = os.environ.get('conversion_server_hostname',
'localhost') 'localhost')
conversion_server_port = os.environ.get('conversion_server_port', conversion_server_port = os.environ.get('conversion_server_port',
'8008') '8008')
return dict(hostname=conversion_server_hostname, return dict(hostname=conversion_server_hostname,
port=int(conversion_server_port)) port=int(conversion_server_port))
def _getVolatileMemcachedServerDict(): def _getVolatileMemcachedServerDict():
"""Returns a dict with hostname and port for volatile memcached Server """Returns a dict with hostname and port for volatile memcached Server
""" """
hostname = os.environ.get('volatile_memcached_server_hostname', hostname = os.environ.get('volatile_memcached_server_hostname',
'localhost') 'localhost')
port = os.environ.get('volatile_memcached_server_port', '11211') port = os.environ.get('volatile_memcached_server_port', '11211')
return dict(hostname=hostname, port=port) return dict(hostname=hostname, port=port)
...@@ -166,7 +164,7 @@ def _getVolatileMemcachedServerDict(): ...@@ -166,7 +164,7 @@ def _getVolatileMemcachedServerDict():
def _getPersistentMemcachedServerDict(): def _getPersistentMemcachedServerDict():
"""Returns a dict with hostname and port for persistent memcached Server """Returns a dict with hostname and port for persistent memcached Server
""" """
hostname = os.environ.get('persistent_memcached_server_hostname', hostname = os.environ.get('persistent_memcached_server_hostname',
'localhost') 'localhost')
port = os.environ.get('persistent_memcached_server_port', '12121') port = os.environ.get('persistent_memcached_server_port', '12121')
return dict(hostname=hostname, port=port) return dict(hostname=hostname, port=port)
...@@ -478,7 +476,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase): ...@@ -478,7 +476,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
if searchable_business_template_list is None: if searchable_business_template_list is None:
searchable_business_template_list = ["erp5_base"] searchable_business_template_list = ["erp5_base"]
# Assume that the public official repository is a valid repository # Assume that the public official repository is a valid repository
public_bt5_repository_list = ['http://www.erp5.org/dists/snapshot/bt5/'] public_bt5_repository_list = ['http://www.erp5.org/dists/snapshot/bt5/']
template_list = [] template_list = []
...@@ -620,7 +618,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase): ...@@ -620,7 +618,7 @@ class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
from AccessControl.SecurityManagement import getSecurityManager from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.SecurityManagement import setSecurityManager from AccessControl.SecurityManagement import setSecurityManager
# Save current security manager # Save current security manager
sm = getSecurityManager() sm = getSecurityManager()
...@@ -703,7 +701,7 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin): ...@@ -703,7 +701,7 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
def _app(self): def _app(self):
'''Opens a ZODB connection and returns the app object. '''Opens a ZODB connection and returns the app object.
We override it to patch HTTP_ACCEPT_CHARSET into REQUEST to get the zpt We override it to patch HTTP_ACCEPT_CHARSET into REQUEST to get the zpt
unicode conflict resolver to work properly''' unicode conflict resolver to work properly'''
app = PortalTestCase._app(self) app = PortalTestCase._app(self)
...@@ -818,7 +816,8 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin): ...@@ -818,7 +816,8 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
if update_business_templates and erp5_load_data_fs: if update_business_templates and erp5_load_data_fs:
update_only = os.environ.get('update_only', None) update_only = os.environ.get('update_only', None)
template_list = (erp5_catalog_storage, 'erp5_property_sheets', template_list = (erp5_catalog_storage, 'erp5_property_sheets',
'erp5_core', 'erp5_xhtml_style') + tuple(template_list) 'erp5_core', 'erp5_xhtml_style') \
+ tuple(template_list)
# Update only specified business templates, regular expression # Update only specified business templates, regular expression
# can be used. # can be used.
if update_only is not None: if update_only is not None:
...@@ -837,6 +836,10 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin): ...@@ -837,6 +836,10 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
light_install = self.enableLightInstall() light_install = self.enableLightInstall()
create_activities = self.enableActivityTool() create_activities = self.enableActivityTool()
hot_reindexing = self.enableHotReindexing() hot_reindexing = self.enableHotReindexing()
# We want to always have optimisation available
if "erp5_stock_cache" not in template_list:
template_list = list(template_list)
template_list.append("erp5_stock_cache")
self.setUpERP5Site(business_template_list=template_list, self.setUpERP5Site(business_template_list=template_list,
light_install=light_install, light_install=light_install,
create_activities=create_activities, create_activities=create_activities,
......
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