diff --git a/product/ERP5/Tool/SimulationTool.py b/product/ERP5/Tool/SimulationTool.py index 5e01f5d543ff05fb426108682ee3d55d20c486a0..aa1400f220da4eed2c5d228b0d3b82e6c1fb5f3e 100644 --- a/product/ERP5/Tool/SimulationTool.py +++ b/product/ERP5/Tool/SimulationTool.py @@ -451,7 +451,8 @@ class SimulationTool (BaseTool): if kw.get('group_by_mirror_node',0): group_by_expression_list.append('%s.mirror_node_uid' % table) if len(group_by_expression_list): - group_by_expression_list.append('%s.resource_uid' % table) # Always group by resource + # Always group by resource + group_by_expression_list.append('%s.resource_uid' % table) sql_kw['group_by_expression'] = ', '.join(group_by_expression_list) sql_kw.update(self.portal_catalog.buildSQLQuery(**new_kw)) @@ -463,7 +464,8 @@ class SimulationTool (BaseTool): 'getInventory') def getInventory(self, src__=0, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, - selection_domain=None, selection_report=None, **kw): + selection_domain=None, selection_report=None, + precision=None, **kw): """ Returns an inventory of a single or multiple resources on a single or multiple nodes as a single float value @@ -513,8 +515,8 @@ class SimulationTool (BaseTool): section_filter - only take rows in stock table which section_uid matches section_filter - mirror_section_filter - only take rows in stock table which mirror_section_uid - matches mirror_section_filter + mirror_section_filter - only take rows in stock table which + mirror_section_uid matches mirror_section_filter variation_text - only take rows in stock table with specified variation_text. @@ -568,6 +570,8 @@ class SimulationTool (BaseTool): group_by_sub_variation - (useless on getInventory, but useful on getInventoryList) + precision - the precision used to round quantities and prices. + **kw - if we want extended selection with more keywords (but bad performance) check what we can do with buildSqlQuery @@ -591,7 +595,7 @@ class SimulationTool (BaseTool): standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, selection_report=selection_report, - **sql_kw) + precision=precision, **sql_kw) if src__: return result @@ -640,7 +644,8 @@ class SimulationTool (BaseTool): 'getInventoryList') def getInventoryList(self, src__=0, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, - selection_domain=None, selection_report=None, **kw): + selection_domain=None, selection_report=None, + precision=None, **kw): """ Returns a list of inventories for a single or multiple resources on a single or multiple nodes, grouped by resource, @@ -656,7 +661,8 @@ class SimulationTool (BaseTool): standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, - selection_report=selection_report, **sql_kw) + selection_report=selection_report, precision=precision, + **sql_kw) security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryList') @@ -694,7 +700,8 @@ class SimulationTool (BaseTool): 'getInventoryStat') def getInventoryStat(self, src__=0, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, - selection_domain=None, selection_report=None, **kw): + selection_domain=None, selection_report=None, + precision=None, **kw): """ getInventoryStat is the pending to getInventoryList in order to provide statistics on getInventoryList lines in ListBox such as: @@ -708,7 +715,8 @@ class SimulationTool (BaseTool): standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, - selection_report=selection_report, **sql_kw) + selection_report=selection_report, + precision=precision, **sql_kw) return result security.declareProtected(Permissions.AccessContentsInformation, @@ -786,16 +794,10 @@ class SimulationTool (BaseTool): def getInventoryAssetPrice(self, src__=0, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, - selection_report=None, **kw): + selection_report=None, precision=None, **kw): """ Same thing as getInventory but returns an asset price rather than an inventory. - - TODO: - - Make sure getInventoryAssetPrice API can - support precision defition (ie. calculate the - sum of rounded values) - """ sql_kw = self._generateSQLKeywordDict(**kw) result = self.Resource_zGetInventory( @@ -803,7 +805,7 @@ class SimulationTool (BaseTool): standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, selection_report=selection_report, - **sql_kw) + precision=precision, **sql_kw) if src__ : return result @@ -853,7 +855,7 @@ class SimulationTool (BaseTool): def getInventoryHistoryList(self, src__=0, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, - selection_report=None, **kw): + selection_report=None, precision=None, **kw): """ Returns a time based serie of inventory values for a single or a group of resource, node, section, etc. This is useful @@ -869,7 +871,8 @@ class SimulationTool (BaseTool): standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, - selection_report=selection_report, **sql_kw) + selection_report=selection_report, precision=precision, + **sql_kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryHistoryChart') @@ -877,7 +880,7 @@ class SimulationTool (BaseTool): standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, - selection_report=None, **kw): + selection_report=None, precision=None, **kw): """ getInventoryHistoryChart is the pensing to getInventoryHistoryList to ease the rendering of time based graphs which show the evolution @@ -892,7 +895,8 @@ class SimulationTool (BaseTool): standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, - selection_report=selection_report, **sql_kw) + selection_report=selection_report, precision=precision, + **sql_kw) security.declareProtected(Permissions.AccessContentsInformation, 'getMovementHistoryList') @@ -901,7 +905,7 @@ class SimulationTool (BaseTool): omit_input=0, omit_output=0, selection_domain=None, selection_report=None, initial_running_total_quantity=0, - initial_running_total_price=0, + initial_running_total_price=0, precision=None, **kw): """Returns a list of movements which modify the inventory for a single or a group of resource, node, section, etc. @@ -921,14 +925,14 @@ class SimulationTool (BaseTool): initial_running_total_quantity, initial_running_total_price= initial_running_total_price, - **sql_kw) + precision=precision, **sql_kw) security.declareProtected(Permissions.AccessContentsInformation, 'getMovementHistoryStat') def getMovementHistoryStat(self, src__=0, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, - selection_report=None, **kw): + selection_report=None, precision=None, **kw): """ getMovementHistoryStat is the pending to getMovementHistoryList for ListBox stat @@ -938,7 +942,7 @@ class SimulationTool (BaseTool): ignore_variation=ignore_variation, standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, - selection_report=selection_report, **sql_kw) + selection_report=selection_report, precision=precision, **sql_kw) security.declareProtected(Permissions.AccessContentsInformation, 'getNextNegativeInventoryDate') def getNextNegativeInventoryDate(self, src__=0, diff --git a/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetInventory.xml b/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetInventory.xml index c5aaca1d023524a901199eeaba7550b853c98a26..65d9c4c34721e9282928edf25ec50a4c33f78e80 100644 --- a/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetInventory.xml +++ b/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetInventory.xml @@ -101,6 +101,12 @@ </dictionary> </value> </item> + <item> + <key> <string>precision</string> </key> + <value> + <dictionary/> + </value> + </item> <item> <key> <string>section_filtered</string> </key> <value> @@ -138,20 +144,21 @@ <key> <string>_keys</string> </key> <value> <list> - <string>from_table_list</string> - <string>where_expression</string> - <string>order_by_expression</string> - <string>selection_domain</string> - <string>selection_report</string> - <string>ignore_variation</string> - <string>standardize</string> - <string>omit_simulation</string> - <string>omit_input</string> - <string>omit_output</string> - <string>section_filtered</string> - <string>input_simulation_state</string> - <string>output_simulation_state</string> - <string>group_by_expression</string> +<string>from_table_list</string> +<string>where_expression</string> +<string>order_by_expression</string> +<string>selection_domain</string> +<string>selection_report</string> +<string>ignore_variation</string> +<string>standardize</string> +<string>omit_simulation</string> +<string>omit_input</string> +<string>omit_output</string> +<string>section_filtered</string> +<string>input_simulation_state</string> +<string>output_simulation_state</string> +<string>group_by_expression</string> +<string>precision</string> </list> </value> </item> @@ -181,7 +188,8 @@ omit_output\r\n section_filtered\r\n input_simulation_state:list\r\n output_simulation_state:list\r\n -group_by_expression</string> </value> +group_by_expression\r\n +precision</string> </value> </item> <item> <key> <string>cache_time_</string> </key> @@ -222,9 +230,15 @@ group_by_expression</string> </value> <value> <string encoding="cdata"><![CDATA[ SELECT\n - SUM(ROUND(stock.quantity,2)) AS inventory,\n - SUM(ROUND(stock.quantity,2)) AS total_quantity,\n - SUM(ROUND(stock.total_price,2)) AS total_price,\n +<dtml-if expr="precision is not None">\n + SUM(ROUND(stock.quantity, <dtml-var precision>)) AS inventory,\n + SUM(ROUND(stock.quantity, <dtml-var precision>)) AS total_quantity,\n + SUM(ROUND(stock.total_price, <dtml-var precision>)) AS total_price,\n +<dtml-else>\n + SUM(stock.quantity) AS inventory,\n + SUM(stock.quantity) AS total_quantity,\n + SUM(stock.total_price) AS total_price,\n +</dtml-if>\n COUNT(DISTINCT node.title) AS node_title,\n COUNT(DISTINCT node.relative_url) AS node_relative_url,\n COUNT(DISTINCT section.title) AS section_title,\n @@ -353,9 +367,15 @@ ORDER BY\n <value> <string encoding="cdata"><![CDATA[ SELECT\n - SUM(ROUND(stock.quantity,2)) AS inventory,\n - SUM(ROUND(stock.quantity,2)) AS total_quantity,\n - SUM(ROUND(stock.total_price,2)) AS total_price,\n +<dtml-if expr="precision is not None">\n + SUM(ROUND(stock.quantity, <dtml-var precision>)) AS inventory,\n + SUM(ROUND(stock.quantity, <dtml-var precision>)) AS total_quantity,\n + SUM(ROUND(stock.total_price, <dtml-var precision>)) AS total_price,\n +<dtml-else>\n + SUM(stock.quantity) AS inventory,\n + SUM(stock.quantity) AS total_quantity,\n + SUM(stock.total_price) AS total_price,\n +</dtml-if>\n COUNT(DISTINCT node.title) AS node_title,\n COUNT(DISTINCT node.relative_url) AS node_relative_url,\n COUNT(DISTINCT section.title) AS section_title,\n diff --git a/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetMovementHistoryList.xml b/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetMovementHistoryList.xml index 75ee11a8a0d325dc6edef59ef6287e323f4b56db..fda172ea9fe4bb04dbb63c305d8709193c6f273e 100644 --- a/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetMovementHistoryList.xml +++ b/product/ERP5/bootstrap/erp5_core/SkinTemplateItem/portal_skins/erp5_core/Resource_zGetMovementHistoryList.xml @@ -113,6 +113,12 @@ </dictionary> </value> </item> + <item> + <key> <string>precision</string> </key> + <value> + <dictionary/> + </value> + </item> <item> <key> <string>section_filtered</string> </key> <value> @@ -150,22 +156,23 @@ <key> <string>_keys</string> </key> <value> <list> - <string>from_table_list</string> - <string>where_expression</string> - <string>order_by_expression</string> - <string>group_by_expression</string> - <string>selection_domain</string> - <string>selection_report</string> - <string>ignore_variation</string> - <string>standardize</string> - <string>omit_simulation</string> - <string>omit_input</string> - <string>omit_output</string> - <string>section_filtered</string> - <string>initial_running_total_quantity</string> - <string>initial_running_total_price</string> - <string>input_simulation_state</string> - <string>output_simulation_state</string> +<string>from_table_list</string> +<string>where_expression</string> +<string>order_by_expression</string> +<string>group_by_expression</string> +<string>selection_domain</string> +<string>selection_report</string> +<string>ignore_variation</string> +<string>standardize</string> +<string>omit_simulation</string> +<string>omit_input</string> +<string>omit_output</string> +<string>section_filtered</string> +<string>initial_running_total_quantity</string> +<string>initial_running_total_price</string> +<string>input_simulation_state</string> +<string>output_simulation_state</string> +<string>precision</string> </list> </value> </item> @@ -606,7 +613,8 @@ section_filtered\r\n initial_running_total_quantity\r\n initial_running_total_price\r\n input_simulation_state:list\r\n -output_simulation_state:list</string> </value> +output_simulation_state:list\r\n +precision</string> </value> </item> <item> <key> <string>cache_time_</string> </key> @@ -662,8 +670,13 @@ SELECT\n catalog.uid as uid,\n catalog.relative_url as relative_url,\n stock.date AS date,\n +<dtml-if expr="precision is not None">\n + ROUND(stock.quantity, <dtml-var precision>) AS total_quantity,\n + ROUND(stock.total_price, <dtml-var precision>) AS total_price,\n +<dtml-else>\n stock.quantity AS total_quantity,\n stock.total_price AS total_price,\n +</dtml-if>\n stock.variation_text AS variation_text,\n stock.simulation_state AS simulation_state,\n stock.mirror_section_uid AS mirror_section_uid,\n @@ -814,8 +827,13 @@ SELECT\n catalog.uid as uid,\n catalog.relative_url as relative_url,\n stock.date AS date,\n +<dtml-if expr="precision is not None">\n + ROUND(stock.quantity, <dtml-var precision>) AS total_quantity,\n + ROUND(stock.total_price, <dtml-var precision>) AS total_price,\n +<dtml-else>\n stock.quantity AS total_quantity,\n stock.total_price AS total_price,\n +</dtml-if>\n stock.variation_text AS variation_text,\n stock.simulation_state AS simulation_state,\n stock.mirror_section_uid AS mirror_section_uid,\n diff --git a/product/ERP5/bootstrap/erp5_core/bt/revision b/product/ERP5/bootstrap/erp5_core/bt/revision index 396054452291e2e07fcd9f33cd76ea228a88aa3a..c5a644422fb8f683632c43a9973c6f9bf3d5a12e 100644 --- a/product/ERP5/bootstrap/erp5_core/bt/revision +++ b/product/ERP5/bootstrap/erp5_core/bt/revision @@ -1 +1 @@ -220 \ No newline at end of file +221 \ No newline at end of file diff --git a/product/ERP5/tests/testInventoryAPI.py b/product/ERP5/tests/testInventoryAPI.py index 7a362ae2da49ca8019749ea9cbce5732e06fd93a..adf08788448bf20fa0ee7ec7f75c581af6901eea 100644 --- a/product/ERP5/tests/testInventoryAPI.py +++ b/product/ERP5/tests/testInventoryAPI.py @@ -73,7 +73,7 @@ class InventoryAPITestCase(ERP5TypeTestCase): self.portal = self.getPortal() if not hasattr(self.portal, 'testing_folder'): self.portal.newContent(portal_type='Folder', - id='testing_folder') + id='testing_folder') self.folder = self.portal.testing_folder self.section = self._makeOrganisation(title='Section') @@ -472,6 +472,31 @@ class TestInventory(InventoryAPITestCase): self.assertEquals(getInventory( section_uid=self.section.getUid()), 100) + def testPrecision(self): + # getInventory supports a precision= argument to specify the precision to + # round + getInventory = self.getSimulationTool().getInventory + getInventoryAssetPrice = self.getSimulationTool().getInventoryAssetPrice + self._makeMovement( quantity=0.1234, price=1 ) + self.assertAlmostEquals(0.123, + getInventory(precision=3, node_uid=self.node.getUid()), + places=3) + self.assertAlmostEquals(0.123, + getInventoryAssetPrice(precision=3, node_uid=self.node.getUid()), + places=3) + + def testPrecisionAndFloatRoundingIssues(self): + # sum([0.1] * 10) != 1.0 but this is not a problem here + getInventory = self.getSimulationTool().getInventory + getInventoryAssetPrice = self.getSimulationTool().getInventoryAssetPrice + self._makeMovement( quantity=1, price=1 ) + for i in range(10): + self._makeMovement( quantity=-0.1, price=1 ) + self.assertEquals(0, getInventory(precision=2, node_uid=self.node.getUid())) + self.assertEquals(0, getInventoryAssetPrice(precision=2, + node_uid=self.node.getUid())) + + class TestInventoryList(InventoryAPITestCase): """Tests getInventoryList methods. """ @@ -927,6 +952,44 @@ class TestMovementHistoryList(InventoryAPITestCase): node_uid=self.node.getUid(),) self.assertEquals(2, len(mvt_history_list)) self.assertEquals(0, sum([r.total_quantity for r in mvt_history_list])) + + def testPrecision(self): + # getMovementHistoryList supports a precision= argument to specify the + # precision to round + getMovementHistoryList = self.getSimulationTool().getMovementHistoryList + self._makeMovement( quantity=0.1234, price=1 ) + mvt_history_list = getMovementHistoryList( + precision=2, + node_uid=self.node.getUid()) + self.assertEquals(1, len(mvt_history_list)) + self.assertEquals(0.12, mvt_history_list[0].running_total_quantity) + 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_price) + + mvt_history_list = getMovementHistoryList( + precision=3, + node_uid=self.node.getUid()) + self.assertEquals(1, len(mvt_history_list)) + self.assertEquals(0.123, mvt_history_list[0].running_total_quantity) + self.assertEquals(0.123, mvt_history_list[0].running_total_price) + self.assertEquals(0.123, mvt_history_list[0].total_quantity) + self.assertEquals(0.123, mvt_history_list[0].total_price) + + def testPrecisionAndFloatRoundingIssues(self): + # sum([0.1] * 10) != 1.0 but this is not a problem here + getMovementHistoryList = self.getSimulationTool().getMovementHistoryList + date = DateTime() + self._makeMovement( quantity=1, price=1, start_date=date ) + for i in range(10): + self._makeMovement( quantity=-0.1, price=1, start_date=date+i ) + mvt_history_list = getMovementHistoryList( + precision=2, + node_uid=self.node.getUid(), + sort_on=[['stock.date', 'ASC']]) + self.assertEquals(11, len(mvt_history_list)) + self.assertEquals(0, mvt_history_list[-1].running_total_quantity) + self.assertEquals(0, mvt_history_list[-1].running_total_price) class TestInventoryStat(InventoryAPITestCase): """Tests Inventory Stat methods.