diff --git a/product/ERP5/Document/Movement.py b/product/ERP5/Document/Movement.py index 2c37154643e6f4b2490ab99428f964ee7ef87f33..3ff3e39a8c127dff8c2d4b3580d05726ebe210d5 100644 --- a/product/ERP5/Document/Movement.py +++ b/product/ERP5/Document/Movement.py @@ -36,12 +36,12 @@ from Products.ERP5Type.Base import Base #from Products.ERP5.Core import MetaNode, MetaResource from Products.ERP5Type.XMLObject import XMLObject - +from Products.ERP5.mixin.composition import CompositionMixin from Products.ERP5.Document.Amount import Amount from zLOG import LOG, WARNING -class Movement(XMLObject, Amount): +class Movement(XMLObject, Amount, CompositionMixin): """ The Movement class allows to implement ERP5 universal accounting model. diff --git a/product/ERP5/mixin/composition.py b/product/ERP5/mixin/composition.py index 72d055c85c36205893f71a4d7f3eeedfb611a314..044a462c2bee654961fc2f055cb159dec097cdad 100644 --- a/product/ERP5/mixin/composition.py +++ b/product/ERP5/mixin/composition.py @@ -2,6 +2,7 @@ ############################################################################## # # Copyright (c) 2010 Nexedi SA and Contributors. All Rights Reserved. +# Julien Muchembled <jm@nexedi.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsibility of assessing all potential @@ -29,9 +30,12 @@ from AccessControl import ClassSecurityInfo from Acquisition import aq_base from Products.ERP5Type import Permissions +from Products.ERP5Type.Cache import transactional_cached from Products.ERP5.Document.Predicate import Predicate from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery + +@transactional_cached() def _getEffectiveModel(self, start_date=None, stop_date=None): """Return the most appropriate model using effective_date, expiration_date and version number. @@ -69,6 +73,72 @@ def _getEffectiveModel(self, start_date=None, stop_date=None): sort_on=(('version', 'descending'),)) return model_list[0].getObject() + +@transactional_cached() +def _findPredicateList(*container_list): + predicate_list = [] + reference_dict = {} + line_count = 0 + for container in container_list: + for ob in container.contentValues(): + if isinstance(ob, Predicate): + # reference is used to hide lines on farther containers + reference = ob.getProperty('reference') + if reference: + reference_set = reference_dict.setdefault(ob.getPortalType(), set()) + if reference in reference_set: + continue + reference_set.add(reference) + id = str(line_count) + line_count += 1 + predicate_list.append(aq_base(ob.asContext(id=id))) + return predicate_list + + +class _asComposedDocument(object): + """Return a temporary object which is the composition of all effective models + + The returned value is a temporary copy of the given object. The list of all + effective models (specialise values) is stored in a private attribute. + Collecting predicates (from effective models) is done lazily. Predicates can + be accessed through standard Folder API (ex: contentValues). + """ + + def __new__(cls, orig_self): + if '_effective_model_list' in orig_self.__dict__: + return orig_self # if asComposedDocument is called on a composed + # document after any access to its subobjects + self = orig_self.asContext() + self._initBTrees() + base_class = self.__class__ + # this allows to intercept first access to '_folder_handler' + self.__class__ = type(base_class.__name__, (cls, base_class), + {'__module__': base_class.__module__}) + self._effective_model_list = orig_self._findEffectiveSpecialiseValueList() + return self + + def __init__(self, orig_self): + # __new__ does not call __init__ because returned object + # is wrapped in an acquisition context. + assert False + + def asComposedDocument(self): + return self # if asComposedDocument is called on a composed + # document before any access to its subobjects + + @property + def _folder_handler(self): + # restore the original class + # because we don't need to hook _folder_handler anymore + self.__class__ = self.__class__.__bases__[1] + # we filter out objects without any subobject to make the cache of + # '_findPredicateList' useful. Otherwise, the key would be always different + # (starting with 'orig_self'). + for ob in _findPredicateList(*filter(None, self._effective_model_list)): + self._setOb(ob.id, ob) + return self._folder_handler + + class CompositionMixin: """ """ @@ -79,26 +149,10 @@ class CompositionMixin: security.declareProtected(Permissions.AccessContentsInformation, 'asComposedDocument') - def asComposedDocument(self): - container_list = self._findEffectiveSpecialiseValueList() - self = self.asContext() - self._initBTrees() - reference_dict = {} - line_count = 0 - for container in container_list: - for ob in container.contentValues(): - if isinstance(ob, Predicate): - # reference is used to hide lines on farther containers - reference = ob.getProperty('reference') - if reference: - reference_set = reference_dict.setdefault(ob.getPortalType(), set()) - if reference in reference_set: - continue - reference_set.add(reference) - id = str(line_count) - line_count += 1 - self._setOb(id, aq_base(ob.asContext(id=id))) - return self + asComposedDocument = transactional_cached()(_asComposedDocument) + + # XXX add accessors to get properties from '_effective_model_list' ? + # (cf PaySheetModel) def _findEffectiveSpecialiseValueList(self): """Return a list of effective specialised objects that is the @@ -118,9 +172,18 @@ class CompositionMixin: while model_index < len(model_list): model = model_list[model_index] model_index += 1 - for model in map(getEffectiveModel, model.getSpecialiseValueList()): + # we don't use getSpecialiseValueList to avoid acquisition on the parent + for model in map(getEffectiveModel, model.getValueList('specialise')): if model not in model_set: model_set.add(model) - if 1: #model.test(self): + if 1: #model.test(self): # XXX model_list.append(model) + try: + parent_asComposedDocument = self.getParentValue().asComposedDocument + except AttributeError: + pass + else: + model_list += [model + for model in parent_asComposedDocument()._effective_model_list + if model not in model_set] return model_list diff --git a/product/ERP5/tests/testTradeModelLine.py b/product/ERP5/tests/testTradeModelLine.py index c40fc7523708514c065176793155aadfa9feb05a..2ffdc2897c33d96c19ad3b1eecd310549220655a 100644 --- a/product/ERP5/tests/testTradeModelLine.py +++ b/product/ERP5/tests/testTradeModelLine.py @@ -2632,6 +2632,7 @@ return current_movement # change tax trade model line to `movement` level tax.edit(target_level=TARGET_LEVEL_MOVEMENT) + transaction.commit() # flush transactional cache def getTotalAmount(amount_list): result = 0 diff --git a/product/ERP5Type/Base.py b/product/ERP5Type/Base.py index 772a970c549796d3d4f593b3bc9db2ee7f933a00..3cc9d16a7995072c762d22ba893830ad80de3b05 100644 --- a/product/ERP5Type/Base.py +++ b/product/ERP5Type/Base.py @@ -2811,7 +2811,7 @@ class Base( CopyContainer, if k != 'SESSION': setattr(context, k, REQUEST[k]) # Define local properties - if kw is not None: context.__dict__.update(kw) + context.__dict__.update(kw) # Make it a temp content temp_object = TempBase(self.getId()) for k in ('isIndexable', 'reindexObject', 'recursiveReindexObject', 'activate', 'setUid', ):