Commit cd62e742 authored by Jérome Perrin's avatar Jérome Perrin

- 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
parent 6eee2db8
...@@ -36,31 +36,75 @@ from Products.ERP5.Variated import Variated ...@@ -36,31 +36,75 @@ from Products.ERP5.Variated import Variated
class BudgetLine(Predicate, XMLMatrix, 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 sign = self.BudgetLine_getConsumptionSign()
property_sheets = ( PropertySheet.Base budget_dict = dict()
, PropertySheet.XMLObject for brain in self.getPortalObject().portal_simulation\
, PropertySheet.SimpleItem .getCurrentInventoryList(**query_dict):
, PropertySheet.CategoryCore # XXX total_quantity or total_price ??
, PropertySheet.Folder previous_value = budget_dict.get(
, PropertySheet.Predicate budget_model._getCellKeyFromInventoryListBrain(brain, self), 0)
, PropertySheet.SortIndex budget_dict[budget_model._getCellKeyFromInventoryListBrain(brain, self)] = \
, PropertySheet.Task previous_value + brain.total_price * sign
, PropertySheet.Arrow
, PropertySheet.Budget
, PropertySheet.Amount
, PropertySheet.VariationRange
, PropertySheet.Assignment
)
# CMF Type Definition return budget_dict
meta_type='ERP5 Budget Line'
portal_type='Budget Line'
add_permission = Permissions.AddPortalContent
# Declarative security
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.AccessContentsInformation)
...@@ -75,7 +75,7 @@ class BudgetModel(Predicate): ...@@ -75,7 +75,7 @@ class BudgetModel(Predicate):
return cell_range return cell_range
def getInventoryQueryDict(self, budget_cell): 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() query_dict = dict()
for budget_variation in sorted(self.contentValues( for budget_variation in sorted(self.contentValues(
...@@ -83,14 +83,57 @@ class BudgetModel(Predicate): ...@@ -83,14 +83,57 @@ class BudgetModel(Predicate):
key=lambda x:x.getIntIndex()): key=lambda x:x.getIntIndex()):
query_dict.update( query_dict.update(
budget_variation.getInventoryQueryDict(budget_cell)) 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 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): def asBudgetPredicate(self):
" " " "
# XXX predicate for line / cell ? # XXX predicate for line / cell ?
def getBudgetConsumptionMethod(self, budget_cell): def getBudgetConsumptionMethod(self, budget_cell):
# XXX this API might disapear
# XXX return the method, or compute directly ? # XXX return the method, or compute directly ?
budget_consumption_method = None budget_consumption_method = None
for budget_variation in sorted(self.contentValues( for budget_variation in sorted(self.contentValues(
......
...@@ -91,3 +91,74 @@ class BudgetVariation(Predicate): ...@@ -91,3 +91,74 @@ class BudgetVariation(Predicate):
""" """
return {} 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))
...@@ -78,7 +78,14 @@ class CategoryBudgetVariation(BudgetVariation): ...@@ -78,7 +78,14 @@ class CategoryBudgetVariation(BudgetVariation):
base_category = self.getProperty('variation_base_category') base_category = self.getProperty('variation_base_category')
if not base_category: if not base_category:
return dict() 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 ... if '/' not in criterion_category: # safe ...
continue continue
criterion_base_category, category_url = criterion_category.split('/', 1) criterion_base_category, category_url = criterion_category.split('/', 1)
...@@ -94,6 +101,39 @@ class CategoryBudgetVariation(BudgetVariation): ...@@ -94,6 +101,39 @@ class CategoryBudgetVariation(BudgetVariation):
return {axis: criterion_category} return {axis: criterion_category}
return dict() 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): def getBudgetVariationRangeCategoryList(self, context):
"""Returns the Variation Range Category List that can be applied to this """Returns the Variation Range Category List that can be applied to this
budget. budget.
......
...@@ -132,15 +132,23 @@ class NodeBudgetVariation(BudgetVariation): ...@@ -132,15 +132,23 @@ class NodeBudgetVariation(BudgetVariation):
if not base_category: if not base_category:
return dict() return dict()
budget_line = budget_cell.getParentValue() budget_line = budget_cell.getParentValue()
portal = self.getPortalObject()
portal_categories = portal.portal_categories context = budget_cell
for criterion_category in budget_cell.getMembershipCriterionCategoryList(): 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 ... if '/' not in criterion_category: # safe ...
continue continue
criterion_base_category, node_url = criterion_category.split('/', 1) criterion_base_category, node_url = criterion_category.split('/', 1)
if criterion_base_category == base_category: if criterion_base_category == base_category:
if axis == 'movement': if axis == 'movement':
axis = 'default_%s' % base_category axis = 'default_%s' % base_category
# TODO: This is not correct if axis is a category (such as
# section_category)
axis = '%s_uid' % axis axis = '%s_uid' % axis
if node_url == budget_line.getRelativeUrl(): if node_url == budget_line.getRelativeUrl():
# This is the "All Other" virtual node # This is the "All Other" virtual node
...@@ -155,6 +163,51 @@ class NodeBudgetVariation(BudgetVariation): ...@@ -155,6 +163,51 @@ class NodeBudgetVariation(BudgetVariation):
return dict() 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): def getBudgetLineVariationRangeCategoryList(self, budget_line):
"""Returns the Variation Range Category List that can be applied to this """Returns the Variation Range Category List that can be applied to this
......
This diff is collapsed.
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