diff --git a/product/ERP5/Tool/SimulationTool.py b/product/ERP5/Tool/SimulationTool.py index f458a599c55d624729730e20b8bdac68278be29f..a181c1c271edfdc6746bc2354968d6b746dcf280 100644 --- a/product/ERP5/Tool/SimulationTool.py +++ b/product/ERP5/Tool/SimulationTool.py @@ -1209,6 +1209,7 @@ class SimulationTool(BaseTool): 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', @@ -1579,24 +1580,59 @@ class SimulationTool(BaseTool): security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryAssetPrice') def getInventoryAssetPrice(self, src__=0, - simulation_period='', **kw): + simulation_period='', + valuation_method=None, + **kw): """ Same thing as getInventory but returns an asset price rather than an inventory. - """ - method = getattr(self,'get%sInventoryList' % simulation_period) - kw['ignore_group_by'] = 1 - result = method( src__=src__, inventory_list=0, **kw) - if src__ : - return result - total_result = 0.0 - if len(result) > 0: + If valuation method is None, returns the sum of total prices. + + Else it should be a string, in: + Filo + Fifo + WeightedAverage + MonthlyWeightedAverage + MovingAverage + When using a specific valuation method, a resource_uid is expected + as well as one of (section_uid or node_uid). + """ + if valuation_method is None: + method = getattr(self,'get%sInventoryList' % simulation_period) + kw['ignore_group_by'] = 1 + result = method( src__=src__, inventory_list=0, **kw) + if src__ : + return result + + if len(result) == 0: + return 0.0 + + total_result = 0.0 for result_line in result: if result_line.total_price is not None: total_result += result_line.total_price - return total_result + return total_result + + if valuation_method not in ('Fifo', 'Filo', 'WeightedAverage', + 'MonthlyWeightedAverage', 'MovingAverage'): + raise ValueError("Invalid valuation method: %s" % valuation_method) + + assert 'node_uid' in kw or 'section_uid' in kw + sql_kw = self._generateSQLKeywordDict(**kw) + + if 'section_uid' in kw: + # ignore internal movements + sql_kw['where_expression'] += ' AND ' \ + 'stock.section_uid!=stock.mirror_section_uid' + + result = self.Resource_zGetAssetPrice( + valuation_method=valuation_method, + **sql_kw) + + if len(result) > 0: + return result[-1].total_asset_price security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice') diff --git a/product/ERP5/tests/testInventoryAPI.py b/product/ERP5/tests/testInventoryAPI.py index b7f1de81591a6961692d1d0e356605ac0cd8441d..ba88dc8578b6a467b29ce8bf0132d9ca784228b1 100644 --- a/product/ERP5/tests/testInventoryAPI.py +++ b/product/ERP5/tests/testInventoryAPI.py @@ -1038,6 +1038,114 @@ class TestInventoryList(InventoryAPITestCase): checkInventory(line=3, type='Future', source=1, quantity=-9) checkInventory(line=3, type='Future', destination=1, quantity=9) + def test_inventory_asset_price(self): + # examples from http://accountinginfo.com/study/inventory/inventory-120.htm + movement_list = [ + (1, "Beginning Inventory", -700, 10), + (3, "Purchase", -100, 12), + (8, "Sale", 500, None), + (15, "Purchase", -600, 14), + (19, "Purchase", -200, 15), + (25, "Sale", 400, None), + (27, "Sale", 100, None), + ] + resource = self.getProductModule().newContent( + title='My resource', + portal_type='Product') + for m in movement_list: + self._makeMovement(resource_value=resource, + source_value=self.node, + destination_value=self.mirror_node, + start_date=DateTime('2000/1/%d 12:00 UTC' % m[0]), + title=m[1], + quantity=m[2], + price=m[3], + ) + + self._safeTic() + simulation_tool = self.getSimulationTool() + def valuate(method): + r = simulation_tool.getInventoryAssetPrice( + valuation_method=method, + resource_uid=resource.getUid(), + node_uid=self.node.getUid()) + return round(r) + + + self.assertEquals(7895, valuate("MovingAverage")) + self.assertEquals(7200, valuate("Filo")) + self.assertEquals(8600, valuate("Fifo")) + + def test_weighted_average_asset_price(self): + def h(quantity, total_price): + """ + A small helper. Returns a dictionary + """ + d = dict(quantity=quantity, + price=float(total_price)/quantity, + total_price=total_price) + return d + # one item per month: + # - movement_list: quantity is negative, it's incoming/purchase + # - after: quantity, total_price, and expected unit_price + # Data was extracted from existing ledger books + data = { + '2009/11': + dict(movement_list=[h(566, 259208),], after=h(566, 259208),), + '2009/12': + dict(movement_list=[h(600, 291600), h(-1135, 536164), ], + after=h(31, 14644)), + '2010/01': + dict(movement_list=[h(1200, 583200), ], after=h(1231, 597844)), + '2010/02': + dict(movement_list=[h(200, 97200), h(-1265, 614417), ], + after=h(166, 80627)), + '2010/03': + dict(movement_list=[], after=h(166, 80627)), + '2010/04': + dict(movement_list=[h(600, 291600), h(-680, 330437), ], + after=h(86, 41791)), + '2010/05': + dict(movement_list=[], after=h(86, 41791)), + '2010/06': + dict(movement_list=[], after=h(86, 41791)), + '2010/07': + dict(movement_list=[], after=h(86, 41791)), + '2010/08': + dict(movement_list=[h(4400, 2032800), h(-4364, 2018170), ], + after=h(122, 56420)), + '2010/09': + dict(movement_list=[], after=h(122, 56420)), + '2010/10': + dict(movement_list=[], after=h(122, 56420)), + '2010/11': + dict(movement_list=[h(1400, 646800), h(-1357, 626984), h(4, 1848)], + after=h(169, 78084)), + } + + resource = self._makeProduct(title="Product for weighted average test") + resource_uid = resource.getUid() + + # create all movements + for month, value in data.iteritems(): + for mov in value['movement_list']: + d = DateTime('%s/15 15:00 UTC' % month) + self._makeMovement(start_date=d, resource_uid=resource_uid, **mov) + + self._safeTic() + + # and check + for cur in sorted(data)[1:]: + # month+1 + to_date = DateTime("%s/1" % cur) + 31 + + result = self.getSimulationTool().getInventoryAssetPrice( + valuation_method="MonthlyWeightedAverage", + to_date=to_date, + resource_uid=resource.getUid(), + node_uid=self.node.getUid()) + self.assertTrue(result is not None) + self.assertEquals(data[cur]['after']['total_price'], round(result)) class TestMovementHistoryList(InventoryAPITestCase): """Tests Movement history list methods. @@ -2460,6 +2568,7 @@ class TestInventoryDocument(InventoryAPITestCase): optimisation__=False, mirror_uid=self.mirror_node.getUid())]) + class BaseTestUnitConversion(InventoryAPITestCase): QUANTITY_UNIT_DICT = {} METRIC_TYPE_CATEGORY_LIST = ()