From cd62e74285919170112d6fa153325d1576b288eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com> Date: Wed, 21 Jul 2010 14:44:15 +0000 Subject: [PATCH] - Initial implementation of a new way of calulating budget consumptions: instead of doing one getInventory by budget cell, we do one getInventoryList by budget line. - Budget variation now uses the variation defined at the proper level, ie if this is a line level variation, it uses membership criterion from the line, if this is budget level, from the budget. if this is a cell level, from the cell. This means that we no longer have to copy all level categories on the cell. The UI will have to be updated. - Test those new features and add some missing tests git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@37220 20353a03-c40f-0410-a6d1-a30d3c3de9de --- product/ERP5/Document/BudgetLine.py | 90 ++- product/ERP5/Document/BudgetModel.py | 47 +- product/ERP5/Document/BudgetVariation.py | 71 +++ .../ERP5/Document/CategoryBudgetVariation.py | 42 +- product/ERP5/Document/NodeBudgetVariation.py | 59 +- product/ERP5/tests/testBudget.py | 536 +++++++++++++++++- 6 files changed, 805 insertions(+), 40 deletions(-) diff --git a/product/ERP5/Document/BudgetLine.py b/product/ERP5/Document/BudgetLine.py index 46d6922ecc..2e321ce597 100644 --- a/product/ERP5/Document/BudgetLine.py +++ b/product/ERP5/Document/BudgetLine.py @@ -36,31 +36,75 @@ from Products.ERP5.Variated import Variated class BudgetLine(Predicate, XMLMatrix, Variated): + """ A Line of budget, variated in budget cells. + """ + + # Default Properties + property_sheets = ( PropertySheet.Base + , PropertySheet.XMLObject + , PropertySheet.SimpleItem + , PropertySheet.CategoryCore + , PropertySheet.Folder + , PropertySheet.Predicate + , PropertySheet.SortIndex + , PropertySheet.Task + , PropertySheet.Arrow + , PropertySheet.Budget + , PropertySheet.Amount + , PropertySheet.VariationRange + ) + + # CMF Type Definition + meta_type='ERP5 Budget Line' + portal_type='Budget Line' + add_permission = Permissions.AddPortalContent + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getConsumedBudgetDict') + def getConsumedBudgetDict(self, **kw): + """Returns all the consumptions in a dict where the keys are the cells, and + the value is the consumed budget. """ - BudgetLine a line of budget... + return self._getBudgetDict(**kw) + + security.declareProtected(Permissions.AccessContentsInformation, + 'getEngagedBudgetDict') + def getEngagedBudgetDict(self, **kw): + """Returns all the engagements in a dict where the keys are the cells, and + the value is the engaged budget. + """ + kw.setdefault('explanation_simulation_state', + self.getPortalReservedInventoryStateList() + + self.getPortalCurrentInventoryStateList() + + self.getPortalTransitInventoryStateList()) + return self._getBudgetDict(**kw) + + def _getBudgetDict(self, **kw): + """Use getCurrentInventoryList to compute all budget cell consumptions at + once, and returns them in a dict. """ + budget = self.getParentValue() + budget_model = budget.getSpecialiseValue(portal_type='Budget Model') + if budget_model is None: + return dict() + + query_dict = budget_model.getInventoryListQueryDict(self) + query_dict.update(kw) + query_dict.setdefault('ignore_group_by', True) - # Default Properties - property_sheets = ( PropertySheet.Base - , PropertySheet.XMLObject - , PropertySheet.SimpleItem - , PropertySheet.CategoryCore - , PropertySheet.Folder - , PropertySheet.Predicate - , PropertySheet.SortIndex - , PropertySheet.Task - , PropertySheet.Arrow - , PropertySheet.Budget - , PropertySheet.Amount - , PropertySheet.VariationRange - , PropertySheet.Assignment - ) + sign = self.BudgetLine_getConsumptionSign() + budget_dict = dict() + for brain in self.getPortalObject().portal_simulation\ + .getCurrentInventoryList(**query_dict): + # XXX total_quantity or total_price ?? + previous_value = budget_dict.get( + budget_model._getCellKeyFromInventoryListBrain(brain, self), 0) + budget_dict[budget_model._getCellKeyFromInventoryListBrain(brain, self)] = \ + previous_value + brain.total_price * sign - # CMF Type Definition - meta_type='ERP5 Budget Line' - portal_type='Budget Line' - add_permission = Permissions.AddPortalContent + return budget_dict - # Declarative security - security = ClassSecurityInfo() - security.declareObjectProtected(Permissions.AccessContentsInformation) diff --git a/product/ERP5/Document/BudgetModel.py b/product/ERP5/Document/BudgetModel.py index d7a77d0115..9d3e69da63 100644 --- a/product/ERP5/Document/BudgetModel.py +++ b/product/ERP5/Document/BudgetModel.py @@ -75,7 +75,7 @@ class BudgetModel(Predicate): return cell_range def getInventoryQueryDict(self, budget_cell): - """Returns the query dict to pass to simulation query + """Returns the query dict to pass to simulation query for a budget cell """ query_dict = dict() for budget_variation in sorted(self.contentValues( @@ -83,14 +83,57 @@ class BudgetModel(Predicate): key=lambda x:x.getIntIndex()): query_dict.update( budget_variation.getInventoryQueryDict(budget_cell)) + + # include dates from the budget + budget = budget_cell.getParentValue().getParentValue() + query_dict.setdefault('from_date', budget.getStartDateRangeMin()) + start_date_range_max = budget.getStartDateRangeMax() + if start_date_range_max: + query_dict.setdefault('at_date', start_date_range_max.latestTime()) return query_dict + + def getInventoryListQueryDict(self, budget_line): + """Returns the query dict to pass to simulation query for a budget line + """ + query_dict = dict() + for budget_variation in sorted(self.contentValues( + portal_type=self.getPortalBudgetVariationTypeList()), + key=lambda x:x.getIntIndex()): + variation_query_dict = budget_variation.getInventoryListQueryDict(budget_line) + # Merge group_by argument. All other arguments should not conflict + if 'group_by' in query_dict and 'group_by' in variation_query_dict: + variation_query_dict['group_by'].extend(query_dict['group_by']) + + query_dict.update(variation_query_dict) + + # include dates from the budget + budget = budget_line.getParentValue() + query_dict.setdefault('from_date', budget.getStartDateRangeMin()) + start_date_range_max = budget.getStartDateRangeMax() + if start_date_range_max: + query_dict.setdefault('at_date', start_date_range_max.latestTime()) + return query_dict + + def _getCellKeyFromInventoryListBrain(self, brain, budget_line): + """Compute the cell key from an inventory brain, the cell key can be used + to retrieve the budget cell in the corresponding budget line. + """ + cell_key = () + for budget_variation in sorted(self.contentValues( + portal_type=self.getPortalBudgetVariationTypeList()), + key=lambda x:x.getIntIndex()): + key = budget_variation._getCellKeyFromInventoryListBrain(brain, + budget_line) + if key: + cell_key += (key,) + return cell_key def asBudgetPredicate(self): " " # XXX predicate for line / cell ? - def getBudgetConsumptionMethod(self, budget_cell): + # XXX this API might disapear # XXX return the method, or compute directly ? budget_consumption_method = None for budget_variation in sorted(self.contentValues( diff --git a/product/ERP5/Document/BudgetVariation.py b/product/ERP5/Document/BudgetVariation.py index 1f7e8ad94b..34e22e307b 100644 --- a/product/ERP5/Document/BudgetVariation.py +++ b/product/ERP5/Document/BudgetVariation.py @@ -91,3 +91,74 @@ class BudgetVariation(Predicate): """ return {} + def getInventoryListQueryDict(self, budget_line): + """Returns the query dict to pass to simulation query for a budget line + """ + return {} + + def _getCellKeyFromInventoryListBrain(self, brain, budget_line): + """Compute the cell key from an inventory brain. + The cell key can be used to retrieve the budget cell in the corresponding + budget line using budget_line.getCell + """ + if not self.isMemberOf('budget_variation/budget_cell'): + return None + + axis = self.getInventoryAxis() + if not axis: + return None + base_category = self.getProperty('variation_base_category') + if not base_category: + return None + + movement = brain.getObject() + # axis 'movement' is simply a category membership on movements + if axis == 'movement': + return movement.getDefaultAcquiredCategoryMembership(base_category, + base=True) + + # is it a source brain or destination brain ? + is_source_brain = True + if (brain.node_uid != brain.mirror_node_uid): + is_source_brain = (brain.node_uid == movement.getSourceUid()) + elif (brain.section_uid != brain.mirror_section_uid): + is_source_brain = (brain.section_uid == movement.getSourceSectionUid()) + elif brain.total_quantity: + is_source_brain = (brain.total_quantity == movement.getQuantity()) + else: + raise NotImplementedError('Could not guess brain side') + + if axis.endswith('_category') or\ + axis.endswith('_category_strict_membership'): + # if the axis is category, we get the node and then returns the category + # from that node + if axis.endswith('_category'): + axis = axis[:-len('_category')] + if axis.endswith('_category_strict_membership'): + axis = axis[:-len('_category_strict_membership')] + if is_source_brain: + if axis == 'node': + node = movement.getSourceValue() + else: + node = movement.getProperty('source_%s_value' % axis) + else: + if axis == 'node': + node = movement.getDestinationValue() + else: + node = movement.getProperty('destination_%s_value' % axis) + if node is not None: + return node.getDefaultAcquiredCategoryMembership(base_category, + base=True) + return None + + # otherwise we just return the node + if is_source_brain: + if axis == 'node': + return '%s/%s' % (base_category, movement.getSource()) + return '%s/%s' % (base_category, + movement.getProperty('source_%s' % axis)) + if axis == 'node': + return '%s/%s' % (base_category, movement.getDestination()) + return '%s/%s' % (base_category, + movement.getProperty('destination_%s' % axis)) + diff --git a/product/ERP5/Document/CategoryBudgetVariation.py b/product/ERP5/Document/CategoryBudgetVariation.py index de58d1274d..7555cbfa3c 100644 --- a/product/ERP5/Document/CategoryBudgetVariation.py +++ b/product/ERP5/Document/CategoryBudgetVariation.py @@ -78,7 +78,14 @@ class CategoryBudgetVariation(BudgetVariation): base_category = self.getProperty('variation_base_category') if not base_category: return dict() - for criterion_category in budget_cell.getMembershipCriterionCategoryList(): + + context = budget_cell + if self.isMemberOf('budget_variation/budget'): + context = budget_cell.getParentValue().getParentValue() + elif self.isMemberOf('budget_variation/budget_line'): + context = budget_cell.getParentValue() + + for criterion_category in context.getMembershipCriterionCategoryList(): if '/' not in criterion_category: # safe ... continue criterion_base_category, category_url = criterion_category.split('/', 1) @@ -94,6 +101,39 @@ class CategoryBudgetVariation(BudgetVariation): return {axis: criterion_category} return dict() + def getInventoryListQueryDict(self, budget_line): + """Returns the query dict to pass to simulation query for a budget line + """ + axis = self.getInventoryAxis() + if not axis: + return dict() + base_category = self.getProperty('variation_base_category') + if not base_category: + return dict() + + context = budget_line + if self.isMemberOf('budget_variation/budget'): + context = budget_line.getParentValue() + + query_dict = dict() + if axis == 'movement': + axis = 'default_strict_%s_uid' % base_category + query_dict['group_by'] = [axis] + else: + query_dict['group_by_%s' % axis] = True + if axis in ('node', 'section', 'payment', 'function', 'project', + 'mirror_section', 'mirror_node' ): + axis = '%s_uid' % axis + + for category in context.getVariationCategoryList( + base_category_list=(base_category,)): + if axis.endswith('_uid'): + category = self.getPortalObject().portal_categories\ + .getCategoryUid(category) + query_dict.setdefault(axis, []).append(category) + + return query_dict + def getBudgetVariationRangeCategoryList(self, context): """Returns the Variation Range Category List that can be applied to this budget. diff --git a/product/ERP5/Document/NodeBudgetVariation.py b/product/ERP5/Document/NodeBudgetVariation.py index dbbef4f139..0b8b3e9288 100644 --- a/product/ERP5/Document/NodeBudgetVariation.py +++ b/product/ERP5/Document/NodeBudgetVariation.py @@ -132,15 +132,23 @@ class NodeBudgetVariation(BudgetVariation): if not base_category: return dict() budget_line = budget_cell.getParentValue() - portal = self.getPortalObject() - portal_categories = portal.portal_categories - for criterion_category in budget_cell.getMembershipCriterionCategoryList(): + + context = budget_cell + if self.isMemberOf('budget_variation/budget'): + context = budget_line.getParentValue() + elif self.isMemberOf('budget_variation/budget_line'): + context = budget_line + + portal_categories = self.getPortalObject().portal_categories + for criterion_category in context.getMembershipCriterionCategoryList(): if '/' not in criterion_category: # safe ... continue criterion_base_category, node_url = criterion_category.split('/', 1) if criterion_base_category == base_category: if axis == 'movement': axis = 'default_%s' % base_category + # TODO: This is not correct if axis is a category (such as + # section_category) axis = '%s_uid' % axis if node_url == budget_line.getRelativeUrl(): # This is the "All Other" virtual node @@ -155,6 +163,51 @@ class NodeBudgetVariation(BudgetVariation): return dict() + def getInventoryListQueryDict(self, budget_line): + """Returns the query dict to pass to simulation query for a budget line + """ + axis = self.getInventoryAxis() + if not axis: + return dict() + base_category = self.getProperty('variation_base_category') + if not base_category: + return dict() + + context = budget_line + if self.isMemberOf('budget_variation/budget'): + context = budget_line.getParentValue() + + portal_categories = self.getPortalObject().portal_categories + query_dict = dict() + if axis == 'movement': + axis = 'default_%s_uid' % base_category + query_dict['group_by_%s' % axis] = True + # TODO: This is not correct if axis is a category (such as + # section_category) + axis = '%s_uid' % axis + + # if we have a virtual "all others" node, we don't set a criterion here. + if self.getProperty('include_virtual_other_node'): + return query_dict + + for node_url in context.getVariationCategoryList( + base_category_list=(base_category,)): + query_dict.setdefault(axis, []).append( + portal_categories.getCategoryValue(node_url, + base_category=base_category).getUid()) + return query_dict + + def _getCellKeyFromInventoryListBrain(self, brain, budget_line): + """Compute key from inventory brain, with support for "all others" virtual node. + """ + key = BudgetVariation._getCellKeyFromInventoryListBrain( + self, brain, budget_line) + if self.getProperty('include_virtual_other_node'): + if key not in [x[1] for x in + self.getBudgetVariationRangeCategoryList(budget_line)]: + key = '%s/%s' % ( self.getProperty('variation_base_category'), + budget_line.getRelativeUrl() ) + return key def getBudgetLineVariationRangeCategoryList(self, budget_line): """Returns the Variation Range Category List that can be applied to this diff --git a/product/ERP5/tests/testBudget.py b/product/ERP5/tests/testBudget.py index e6d452fc89..5fc89b3d46 100644 --- a/product/ERP5/tests/testBudget.py +++ b/product/ERP5/tests/testBudget.py @@ -28,9 +28,31 @@ import unittest import transaction +from DateTime import DateTime + from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase +from AccessControl import getSecurityManager class TestBudget(ERP5TypeTestCase): + + def afterSetUp(self): + self.validateRules() + product_line = self.portal.portal_categories.product_line + if '1' not in product_line.objectIds(): + category = product_line.newContent(portal_type='Category', id='1') + category.newContent(portal_type='Category', id='1.1') + category.newContent(portal_type='Category', id='1.2') + if '2' not in product_line.objectIds(): + category = product_line.newContent(portal_type='Category', id='2') + category.newContent(portal_type='Category', id='2.1') + category.newContent(portal_type='Category', id='2.2') + + def beforeTearDown(self): + transaction.abort() + self.portal.accounting_module.manage_delObjects( + list(self.portal.accounting_module.objectIds())) + transaction.commit() + self.tic() def getBusinessTemplateList(self): """Return the list of required business templates. @@ -93,21 +115,513 @@ class TestBudget(ERP5TypeTestCase): self.assertEquals(budget_line.getMembershipCriterionCategoryList(), []) self.assertEquals( budget_line.getMembershipCriterionBaseCategoryList(), []) + - # TODO: create cells and test variation on cell - - # Other TODOs - # test simple category variation in getInventory - - # test that using a category variation on budget level sets membership - # criterion on budget + # simuate a request and call Base_edit, which does all the work of creating + # cell and setting cell properties. + form = budget_line.BudgetLine_view + self.portal.REQUEST.other.update( + dict(AUTHENTICATED_USER=getSecurityManager().getUser(), + + field_membership_criterion_base_category_list= + form.membership_criterion_base_category_list.get_value('default'), + field_mapped_value_property_list= + form.mapped_value_property_list.get_value('default'), + + field_matrixbox_quantity_cell_0_0_0="5", + field_matrixbox_membership_criterion_category_list_cell_0_0_0=[ + 'source/account_module/goods_purchase'], + )) + budget_line.Base_edit(form_id=form.getId()) + + self.assertEquals(1, len(budget_line.contentValues())) + budget_cell = budget_line.getCell('source/account_module/goods_purchase') + self.assertNotEquals(None, budget_cell) + + self.assertEquals(['source/account_module/goods_purchase'], + budget_cell.getMembershipCriterionCategoryList()) + self.assertEquals(5, budget_cell.getQuantity()) + + # there is no budget consumption + self.assertEquals(0, budget_cell.getConsumedBudget()) + self.assertEquals(0, budget_cell.getEngagedBudget()) + self.assertEquals(5, budget_cell.getAvailableBudget()) + # there is no budget transfer + self.assertEquals(5, budget_cell.getCurrentBalance()) + + + def test_category_budget_cell_variation(self): + budget_model = self.portal.budget_model_module.newContent( + portal_type='Budget Model') + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=1, + budget_variation='budget_cell', + inventory_axis='node_category', + variation_base_category='account_type',) + budget = self.portal.budget_module.newContent( + portal_type='Budget', + specialise_value=budget_model) + budget_line = budget.newContent(portal_type='Budget Line') + self.assertEquals(['account_type'], + budget_line.getVariationBaseCategoryList()) + + variation_range_category_list = \ + budget_line.BudgetLine_getVariationRangeCategoryList() + self.assertTrue(['', ''] in variation_range_category_list) + self.assertTrue(['Expense', 'account_type/expense'] in variation_range_category_list) + + def test_category_budget_line_variation(self): + # test that using a variation on budget line level sets membership + # criterion on budget line + budget_model = self.portal.budget_model_module.newContent( + portal_type='Budget Model') + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=1, + budget_variation='budget_line', + inventory_axis='section_category', + variation_base_category='group',) + budget = self.portal.budget_module.newContent( + portal_type='Budget', + specialise_value=budget_model) + budget_line = budget.newContent(portal_type='Budget Line') + + self.assertEquals(['group'], + budget_line.getVariationBaseCategoryList()) + + variation_range_category_list = \ + budget_line.BudgetLine_getVariationRangeCategoryList() + + self.assertTrue(['', ''] in variation_range_category_list) + self.assertTrue(['Demo Group', 'group/demo_group'] in variation_range_category_list) + + budget_line.edit(variation_category_list=['group/demo_group']) + self.assertEquals(['group'], + budget_line.getMembershipCriterionBaseCategoryList()) + self.assertEquals(['group/demo_group'], + budget_line.getMembershipCriterionCategoryList()) + + + def test_category_budget_variation(self): + budget_model = self.portal.budget_model_module.newContent( + portal_type='Budget Model') + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=1, + budget_variation='budget', + inventory_axis='section_category', + variation_base_category='group',) + budget = self.portal.budget_module.newContent( + portal_type='Budget', + specialise_value=budget_model) + + self.assertEquals(['group'], + budget.getVariationBaseCategoryList()) + + variation_range_category_list = \ + budget.Budget_getVariationRangeCategoryList() + + self.assertTrue(['', ''] in variation_range_category_list) + self.assertTrue(['Demo Group', 'group/demo_group'] in variation_range_category_list) + + # setting this variation on the budget also sets membership + budget.edit(variation_category_list=['group/demo_group']) + self.assertEquals('demo_group', budget.getGroup()) + self.assertEquals('Demo Group', budget.getGroupTitle()) + + def test_simple_consumption(self): + budget_model = self.portal.budget_model_module.newContent( + portal_type='Budget Model') + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=1, + budget_variation='budget', + inventory_axis='section_category', + variation_base_category='group',) + budget_model.newContent( + portal_type='Node Budget Variation', + int_index=2, + budget_variation='budget_cell', + inventory_axis='node', + variation_base_category='source', + aggregate_value_list=( + self.portal.account_module.goods_purchase, + self.portal.account_module.fixed_assets, + )) + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=3, + budget_variation='budget_cell', + inventory_axis='node_category', + variation_base_category='account_type',) + + budget = self.portal.budget_module.newContent( + portal_type='Budget', + start_date_range_min=DateTime(2000, 1, 1), + start_date_range_max=DateTime(2000, 12, 31), + specialise_value=budget_model) + + budget.edit(variation_category_list=['group/demo_group']) + budget_line = budget.newContent(portal_type='Budget Line') + + # set the range, this will adjust the matrix + budget_line.edit( + variation_category_list=( + 'source/account_module/goods_purchase', + 'source/account_module/fixed_assets', + 'account_type/expense', + 'account_type/asset', )) + + # simuate a request and call Base_edit, which does all the work of creating + # cell and setting cell properties. + form = budget_line.BudgetLine_view + self.portal.REQUEST.other.update( + dict(AUTHENTICATED_USER=getSecurityManager().getUser(), + + field_membership_criterion_base_category_list= + form.membership_criterion_base_category_list.get_value('default'), + field_mapped_value_property_list= + form.mapped_value_property_list.get_value('default'), + + field_matrixbox_quantity_cell_0_0_0="", + field_matrixbox_membership_criterion_category_list_cell_0_0_0=[], + field_matrixbox_quantity_cell_1_0_0="2", + field_matrixbox_membership_criterion_category_list_cell_1_0_0=[ + 'source/account_module/fixed_assets', + 'account_type/asset'], + field_matrixbox_quantity_cell_0_1_0="1", + field_matrixbox_membership_criterion_category_list_cell_0_1_0=[ + 'source/account_module/goods_purchase', + 'account_type/expense'], + field_matrixbox_quantity_cell_1_1_0="", + field_matrixbox_membership_criterion_category_list_cell_1_1_0=[], + )) + budget_line.Base_edit(form_id=form.getId()) + + self.assertEquals(2, len(budget_line.contentValues())) + budget_cell = budget_line.getCell('source/account_module/goods_purchase', + 'account_type/expense') + self.assertNotEquals(None, budget_cell) + self.assertEquals( + dict(from_date=DateTime(2000, 1, 1), + at_date=DateTime(2000, 12, 31).latestTime(), + node_category='account_type/expense', + node_uid=self.portal.account_module.goods_purchase.getUid(), + section_category='group/demo_group',), + budget_model.getInventoryQueryDict(budget_cell)) + + budget_cell = budget_line.getCell('source/account_module/fixed_assets', + 'account_type/asset') + self.assertNotEquals(None, budget_cell) + self.assertEquals( + dict(from_date=DateTime(2000, 1, 1), + at_date=DateTime(2000, 12, 31).latestTime(), + node_category='account_type/asset', + node_uid=self.portal.account_module.fixed_assets.getUid(), + section_category='group/demo_group',), + budget_model.getInventoryQueryDict(budget_cell)) + + self.assertEquals( + dict(from_date=DateTime(2000, 1, 1), + at_date=DateTime(2000, 12, 31).latestTime(), + node_category=['account_type/expense', 'account_type/asset'], + node_uid=[self.portal.account_module.goods_purchase.getUid(), + self.portal.account_module.fixed_assets.getUid()], + section_category=['group/demo_group'], + group_by_node_category=True, + group_by_node=True, + group_by_section_category=True, + ), + budget_model.getInventoryListQueryDict(budget_line)) + + + atransaction = self.portal.accounting_module.newContent( + portal_type='Accounting Transaction', + resource_value=self.portal.currency_module.euro, + source_section_value=self.portal.organisation_module.my_organisation, + start_date=DateTime(2000, 1, 2)) + atransaction.newContent( + portal_type='Accounting Transaction Line', + source_value=self.portal.account_module.goods_purchase, + source_debit=100) + atransaction.newContent( + portal_type='Accounting Transaction Line', + source_value=self.portal.account_module.fixed_assets, + source_credit=100) + atransaction.stop() + + transaction.commit() + self.tic() + + self.assertEquals( + {('source/account_module/fixed_assets', 'account_type/asset'): -100.0, + ('source/account_module/goods_purchase', 'account_type/expense'): 100.0}, + budget_line.getConsumedBudgetDict()) + + self.assertEquals( + {('source/account_module/fixed_assets', 'account_type/asset'): -100.0, + ('source/account_module/goods_purchase', 'account_type/expense'): 100.0}, + budget_line.getEngagedBudgetDict()) + + def test_all_other_and_strict_consumption(self): + # tests consumptions, by using "all other" virtual node on a node budget + # variation, and strict membership on category budget variation + budget_model = self.portal.budget_model_module.newContent( + portal_type='Budget Model') + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=1, + budget_variation='budget', + inventory_axis='section_category_strict_membership', + variation_base_category='group',) + budget_model.newContent( + portal_type='Node Budget Variation', + int_index=2, + budget_variation='budget_cell', + inventory_axis='node', + variation_base_category='source', + aggregate_value_list=( + self.portal.account_module.goods_purchase,), + include_virtual_other_node=True) + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=3, + budget_variation='budget_cell', + inventory_axis='node_category_strict_membership', + variation_base_category='account_type',) + + budget = self.portal.budget_module.newContent( + portal_type='Budget', + start_date_range_min=DateTime(2000, 1, 1), + start_date_range_max=DateTime(2000, 12, 31), + specialise_value=budget_model) + + budget.edit(variation_category_list=['group/demo_group/sub1']) + budget_line = budget.newContent(portal_type='Budget Line') + + # set the range, this will adjust the matrix + budget_line.edit( + variation_category_list=( + 'source/account_module/goods_purchase', + 'source/%s' % budget_line.getRelativeUrl(), # this is 'all others' + 'account_type/expense', + 'account_type/asset', )) + + # simuate a request and call Base_edit, which does all the work of creating + # cell and setting cell properties. + form = budget_line.BudgetLine_view + self.portal.REQUEST.other.update( + dict(AUTHENTICATED_USER=getSecurityManager().getUser(), + + field_membership_criterion_base_category_list= + form.membership_criterion_base_category_list.get_value('default'), + field_mapped_value_property_list= + form.mapped_value_property_list.get_value('default'), + + field_matrixbox_quantity_cell_0_0_0="", + field_matrixbox_membership_criterion_category_list_cell_0_0_0=[], + field_matrixbox_quantity_cell_1_0_0="2", + field_matrixbox_membership_criterion_category_list_cell_1_0_0=[ + 'source/%s' % budget_line.getRelativeUrl(), + 'account_type/asset'], + field_matrixbox_quantity_cell_0_1_0="1", + field_matrixbox_membership_criterion_category_list_cell_0_1_0=[ + 'source/account_module/goods_purchase', + 'account_type/expense'], + field_matrixbox_quantity_cell_1_1_0="", + field_matrixbox_membership_criterion_category_list_cell_1_1_0=[], + )) + budget_line.Base_edit(form_id=form.getId()) + + self.assertEquals(2, len(budget_line.contentValues())) + + self.assertEquals( + dict(from_date=DateTime(2000, 1, 1), + at_date=DateTime(2000, 12, 31).latestTime(), + node_category_strict_membership=['account_type/expense', + 'account_type/asset'], + section_category_strict_membership=['group/demo_group/sub1'], + group_by_node_category_strict_membership=True, + group_by_node=True, + group_by_section_category_strict_membership=True, + ), + budget_model.getInventoryListQueryDict(budget_line)) + + + atransaction = self.portal.accounting_module.newContent( + portal_type='Accounting Transaction', + resource_value=self.portal.currency_module.euro, + source_section_value=self.portal.organisation_module.my_organisation, + start_date=DateTime(2000, 1, 2)) + atransaction.newContent( + portal_type='Accounting Transaction Line', + source_value=self.portal.account_module.goods_purchase, + source_debit=100) + atransaction.newContent( + portal_type='Accounting Transaction Line', + source_value=self.portal.account_module.fixed_assets, + source_credit=100) + atransaction.stop() + + transaction.commit() + self.tic() + + self.assertEquals( + {('source/%s' % budget_line.getRelativeUrl(), 'account_type/asset'): -100.0, + ('source/account_module/goods_purchase', 'account_type/expense'): 100.0}, + budget_line.getConsumedBudgetDict()) + + self.assertEquals( + {('source/%s' % budget_line.getRelativeUrl(), 'account_type/asset'): -100.0, + ('source/account_module/goods_purchase', 'account_type/expense'): 100.0}, + budget_line.getEngagedBudgetDict()) + + + def test_consumption_movement_category(self): + # test for budget consumption using movement category + budget_model = self.portal.budget_model_module.newContent( + portal_type='Budget Model') + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=1, + budget_variation='budget', + inventory_axis='section_category', + variation_base_category='group',) + budget_model.newContent( + portal_type='Node Budget Variation', + int_index=2, + budget_variation='budget_cell', + inventory_axis='node', + variation_base_category='source', + aggregate_value_list=( + self.portal.account_module.goods_purchase, + self.portal.account_module.fixed_assets, + )) + budget_model.newContent( + portal_type='Category Budget Variation', + int_index=3, + budget_variation='budget_cell', + inventory_axis='movement', + variation_base_category='product_line',) + + budget = self.portal.budget_module.newContent( + portal_type='Budget', + start_date_range_min=DateTime(2000, 1, 1), + start_date_range_max=DateTime(2000, 12, 31), + specialise_value=budget_model) + + budget.edit(variation_category_list=['group/demo_group']) + budget_line = budget.newContent(portal_type='Budget Line') + + # set the range, this will adjust the matrix + budget_line.edit( + variation_category_list=( + 'source/account_module/goods_purchase', + 'source/account_module/fixed_assets', + 'product_line/1', + 'product_line/1/1.1', + 'product_line/1/1.2', )) + + # simuate a request and call Base_edit, which does all the work of creating + # cell and setting cell properties. + form = budget_line.BudgetLine_view + self.portal.REQUEST.other.update( + dict(AUTHENTICATED_USER=getSecurityManager().getUser(), + + field_membership_criterion_base_category_list= + form.membership_criterion_base_category_list.get_value('default'), + field_mapped_value_property_list= + form.mapped_value_property_list.get_value('default'), + + # this cell will be a summary cell + field_matrixbox_quantity_cell_0_0_0="2", + field_matrixbox_membership_criterion_category_list_cell_0_0_0=[ + 'source/account_module/goods_purchase', + 'product_line/1'], + field_matrixbox_quantity_cell_1_0_0="", + field_matrixbox_membership_criterion_category_list_cell_1_0_0=[], + field_matrixbox_quantity_cell_0_1_0="2", + field_matrixbox_membership_criterion_category_list_cell_0_1_0=[ + 'source/account_module/goods_purchase', + 'product_line/1/1.1'], + field_matrixbox_quantity_cell_1_1_0="", + field_matrixbox_membership_criterion_category_list_cell_1_1_0=[], + field_matrixbox_quantity_cell_0_2_0="", + field_matrixbox_membership_criterion_category_list_cell_0_2_0=[], + field_matrixbox_quantity_cell_1_2_0="", + field_matrixbox_membership_criterion_category_list_cell_1_2_0=[], + )) + budget_line.Base_edit(form_id=form.getId()) + + self.assertEquals(2, len(budget_line.contentValues())) + + product_line_1 = self.portal.portal_categories.product_line['1'] + product_line_1_11 = product_line_1['1.1'] + product_line_1_12 = product_line_1['1.2'] + + self.assertEquals( + dict(from_date=DateTime(2000, 1, 1), + at_date=DateTime(2000, 12, 31).latestTime(), + node_uid=[self.portal.account_module.goods_purchase.getUid(), + self.portal.account_module.fixed_assets.getUid(),], + default_strict_product_line_uid=[product_line_1.getUid(), + product_line_1_11.getUid(), + product_line_1_12.getUid(),], + section_category=['group/demo_group'], + group_by=['default_strict_product_line_uid'], + group_by_node=True, + group_by_section_category=True, + ), + budget_model.getInventoryListQueryDict(budget_line)) + + + atransaction = self.portal.accounting_module.newContent( + portal_type='Accounting Transaction', + resource_value=self.portal.currency_module.euro, + source_section_value=self.portal.organisation_module.my_organisation, + start_date=DateTime(2000, 1, 2)) + atransaction.newContent( + portal_type='Accounting Transaction Line', + source_value=self.portal.account_module.goods_purchase, + product_line_value=product_line_1_11, + source_debit=100) + atransaction.newContent( + portal_type='Accounting Transaction Line', + source_value=self.portal.account_module.fixed_assets, + product_line_value=product_line_1_12, + source_credit=100) + atransaction.stop() + + transaction.commit() + self.tic() + + self.assertEquals( + {('source/account_module/fixed_assets', 'product_line/1/1.2'): -100.0, + ('source/account_module/goods_purchase', 'product_line/1/1.1'): 100.0, + # summary line is automatically added (TODO) +## ('source/account_module/goods_purchase', 'product_line/1'): 100.0 + }, + budget_line.getConsumedBudgetDict()) + + self.assertEquals( + {('source/account_module/fixed_assets', 'product_line/1/1.2'): -100.0, + ('source/account_module/goods_purchase', 'product_line/1/1.1'): 100.0, + # summary line is automatically added (TODO) +## ('source/account_module/goods_purchase', 'product_line/1'): 100.0 + }, + budget_line.getEngagedBudgetDict()) + + + # Other TODOs: + + # section_category & summary - # test that using a variation on budget line level sets membership - # criterion on budget line (and budget cell or not ?) + # resource/price currency on budget ? + # test virtual all others when cloning an existing budget - # test that using a category variation on budget level is used in inventory - # calculation + # predicates def test_suite(): -- 2.30.9