diff --git a/product/ERP5/Document/ProductionOrderRule.py b/product/ERP5/Document/ProductionOrderRule.py index ae7862c2ad1c5d085be6aefff27c8a4fb36c27b5..f8f268b70ab5d015db364bdec50c9ce46ff95009 100644 --- a/product/ERP5/Document/ProductionOrderRule.py +++ b/product/ERP5/Document/ProductionOrderRule.py @@ -30,11 +30,12 @@ from AccessControl import ClassSecurityInfo from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces from Products.ERP5.Document.Rule import Rule from Products.ERP5.Document.OrderRule import OrderRule -from Products.ERP5.Document.TransformationRule import TransformationRuleMixin +from Products.ERP5.Document.TransformationSourcingRule import\ + TransformationSourcingRuleMixin from zLOG import LOG, WARNING -class ProductionOrderRule(TransformationRuleMixin, OrderRule): +class ProductionOrderRule(OrderRule): """ Prouction Order Rule object use a Supply Chain to expand a Production Order. @@ -48,6 +49,9 @@ class ProductionOrderRule(TransformationRuleMixin, OrderRule): security = ClassSecurityInfo() security.declareObjectProtected(Permissions.AccessContentsInformation) + __implements = ( interfaces.IPredicate, + interfaces.IRule ) + # Default Properties property_sheets = ( PropertySheet.Base , PropertySheet.XMLObject @@ -89,17 +93,25 @@ class ProductionOrderRule(TransformationRuleMixin, OrderRule): 'quantity_unit', ) - root_explanation = self.getRootExplanation( - self.getBusinessProcess(applied_rule=applied_rule)) - property_dict['source_section'] = root_explanation.getSourceSection() - source_method_id = root_explanation.getSourceMethodId() - if source_method_id is None: - property_dict['source'] = root_explanation.getSource() - else: - property_dict['source'] = getattr(root_explanation, source_method_id)() - property_dict['causality'] = root_explanation.getRelativeUrl() - + supply_chain = self.getSupplyChain(applied_rule) + # We got a supply chain + # Try to get the last SupplyLink + last_link = supply_chain.getLastLink() + # We got a valid industrial_phase + # Now, we have to generate Simulation Movement, in order to + # create a ProductionPackingList. + destination_node = last_link.getDestinationValue() + source_value = destination_node.getDestination() + source_section_value = last_link.getDestinationSection() + if source_value is not None: + property_dict["source"] = source_value + if source_section_value is not None: + property_dict["source_section"] = source_section_value + for prop in default_property_list: property_dict[prop] = movement.getProperty(prop) return property_dict + +from Products.ERP5Type.Utils import monkeyPatch +monkeyPatch(TransformationSourcingRuleMixin, ProductionOrderRule) diff --git a/product/ERP5/Document/TransformationRule.py b/product/ERP5/Document/TransformationRule.py index 02fe9d6bb377035af29e16bd9e90acc5f8d56c88..b5b9f0987494eeb25f760083024974c743a347d2 100644 --- a/product/ERP5/Document/TransformationRule.py +++ b/product/ERP5/Document/TransformationRule.py @@ -1,7 +1,6 @@ -# -*- coding:utf-8 -*- ############################################################################## # -# Copyright (c) 2002-2009 Nexedi SARL and Contributors. All Rights Reserved. +# Copyright (c) 2002, 2005 Nexedi SARL and Contributors. All Rights Reserved. # Jean-Paul Smets-Solanes <jp@nexedi.com> # Romain Courteaud <romain@nexedi.com> # @@ -28,411 +27,298 @@ # ############################################################################## -from ExtensionClass import Base from AccessControl import ClassSecurityInfo from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire from Products.CMFCore.utils import getToolByName from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces from Products.ERP5.Document.Rule import Rule -from Products.ERP5.Document.SimulationMovement import SimulationMovement from Products.ERP5Type.Errors import TransformationRuleError +from Products.ERP5.Document.TransformationSourcingRule import\ + TransformationSourcingRuleMixin -class MovementFactory: - def getRequestList(self): - """ - return the list of a request which to be used to apply movements - """ - raise NotImplementedError, 'Must be implemented' - - def _getCausalityList(self, causality=None, causality_value=None, - causality_list=None, causality_value_list=None, - **kw): - if causality is not None: - return [causality] - elif causality_value is not None: - return [causality_value.getRelativeUrl()] - elif causality_list is not None: - return causality_list - elif causality_value_list is not None: - return [causality_value.getRelativeUrl() - for causality_value in causality_value_list] +from zLOG import LOG - def makeMovements(self, applied_rule): +class TransformationRule(TransformationSourcingRuleMixin, Rule): """ - make movements under the applied_rule by requests - """ - movement_dict = {} - for movement in applied_rule.objectValues( - portal_type="Simulation Movement"): - key = tuple(sorted(movement.getCausalityList())) - movement_dict[key] = movement - - for request in self.getRequestList(): - # get movement by causality - key = tuple(sorted(self._getCausalityList(**request))) - movement = movement_dict.get(key, None) - # when no exist - if movement is None: - movement = applied_rule.newContent(portal_type="Simulation Movement") - # update - if movement.isFrozen(): - self.makeDifferentMovement(movement, **request) - else: - movement.edit(**request) - - def _requestNetQuantity(self, request): - quantity = request.get('quantity', None) - efficiency = request.get('efficiency', None) - if efficiency in (0, 0.0, None, ''): - efficiency = 1.0 - return float(quantity) / efficiency - - def makeDifferentMovement(self, movement, **request): - """ - make different movement, which is based on original movement. - this implementation just focus about quantity. - """ - applied_rule = movement.getParentValue() - request['quantity'] = self._requestNetQuantity(request)\ - - movement.getNetQuantity() - if request['quantity'] != 0: - diff_movement = applied_rule.newContent(portal_type="Simulation Movement") - diff_movement.edit(**request) - - -class TransformationMovementFactory(MovementFactory): - def __init__(self): - self.product = None # base information to use for making movements - self.produced_list = list() - self.consumed_list = list() - - def requestProduced(self, **produced): - self.produced_list.append(produced) - - def requestConsumed(self, **consumed): - self.consumed_list.append(consumed) - - def getRequestList(self): - """ - return the list of a request which to be used to apply movements - """ - _list = [] - """ - produced quantity should be represented by minus quantity on movement. - because plus quantity is consumed. - """ - for (request_list, sign) in ((self.produced_list, -1), - (self.consumed_list, 1)): - for request in request_list: - d = self.product.copy() - d.update(request) - d['quantity'] *= sign - _list.append(d) - return _list - - -class TransformationRuleMixin(Base): - security = ClassSecurityInfo() - - security.declareProtected(Permissions.View, 'getTransformation') - def getTransformation(self, movement=None, applied_rule=None): + Order Rule object make sure an Order in the similation + is consistent with the real order """ - Return transformation related to used by the applied rule. - """ - if movement is None and applied_rule is not None: - movement = applied_rule.getParentValue() - - order_movement = movement.getRootSimulationMovement().getOrderValue() - explanation = self.getExplanation(movement=movement, - applied_rule=applied_rule) - # find line recursively - order_line = order_movement - while order_line.getParentValue() != explanation: - order_line = order_line.getParentValue() - - script = order_line._getTypeBasedMethod('_getTransformation') - if script is not None: - transformation = script() - else: - line_transformation = order_line.objectValues( - portal_type=self.getPortalTransformationTypeList()) - if len(line_transformation) == 1: - transformation = line_transformation[0] - else: - transformation = order_line.getSpecialiseValue( - portal_type=self.getPortalTransformationTypeList()) - - if transformation.getResource() == movement.getResource(): - return transformation - - security.declareProtected(Permissions.View, 'getBusinessProcess') - def getBusinessProcess(self, **kwargs): - """ - Return business process related to root causality. - """ - explanation = self.getExplanation(**kwargs) - if explanation is not None: - specialise = explanation.getSpecialiseValue() - business_process_type_list = self.getPortalBusinessProcessTypeList() - # because trade condition can be specialised - while specialise is not None and \ - specialise.getPortalType() not in business_process_type_list: - specialise = specialise.getSpecialiseValue() - return specialise - - security.declareProtected(Permissions.View, 'getRootExplanation') - def getRootExplanation(self, business_process): - """ - the method of ProductionOrderRule returns most tail path of business process - """ - if business_process is not None: - for business_path in business_process.contentValues( - portal_type=self.getPortalBusinessPathTypeList()): - if business_path.isDeliverable(): - return business_path - - security.declareProtected(Permissions.View, 'getExplanation') - def getExplanation(self, movement=None, applied_rule=None): - if applied_rule is not None: - return applied_rule.getRootAppliedRule().getCausalityValue() - else: - return movement.getRootSimulationMovement()\ - .getOrderValue().getExplanationValue() - - -class TransformationRule(TransformationRuleMixin, Rule): - """ - """ - - # CMF Type Definition - meta_type = 'ERP5 Transformation Rule' - portal_type = 'Transformation Rule' - # Declarative security - security = ClassSecurityInfo() - security.declareObjectProtected(Permissions.AccessContentsInformation) - - # Default Properties - property_sheets = ( PropertySheet.Base + # CMF Type Definition + meta_type = 'ERP5 Transformation Rule' + portal_type = 'Transformation Rule' + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + __implements__ = ( interfaces.IPredicate, + interfaces.IRule ) + # Default Properties + property_sheets = ( PropertySheet.Base , PropertySheet.XMLObject , PropertySheet.CategoryCore , PropertySheet.DublinCore , PropertySheet.Task ) + # Class variable + simulation_movement_portal_type = "Simulation Movement" - def getHeadProductionPathList(self, transformation, business_process): - """ - Return list of path which is head of transformation trade_phases - - this method assumes trade_phase of head paths is only one - """ - production_trade_phase_set = set([amount.getTradePhase() - for amount in transformation\ - .getAggregatedAmountList()]) - head_path_list = [] - for state in business_process.objectValues( - portal_type=self.getPortalBusinessStateTypeList()): - if len(state.getSuccessorRelatedValueList()) == 0: - head_path_list.extend(state.getPredecessorRelatedValueList()) - - result_list = [] - for path in head_path_list: - result_list += self._getHeadPathByTradePhaseList(path, production_trade_phase_set) - - return map(lambda t: t[0], filter(lambda t: t != (None, None), result_list)) - - def _getHeadPathByTradePhaseList(self, path, trade_phase_set): - _set = set(path.getTradePhaseList()) - if _set & trade_phase_set: - return [(path, _set & trade_phase_set)] - - successor_node = path.getSuccessorValue() - if successor_node is None: - return [(None, None)] - - _list = [] - for next_path in successor_node.getPredecessorRelatedValueList(): - _list += self._getHeadPathByTradePhaseList(next_path, trade_phase_set) - return _list - - def getFactory(self): - return TransformationMovementFactory() - - security.declareProtected(Permissions.ModifyPortalContent, 'expand') - def expand(self, applied_rule, **kw): - """ - """ - parent_movement = applied_rule.getParentValue() - - transformation = self.getTransformation(movement=parent_movement) - business_process = self.getBusinessProcess(movement=parent_movement) - explanation = self.getExplanation(movement=parent_movement) - - # get all trade_phase of the Business Process - trade_phase_list = business_process.getTradePhaseList() - - # get head of production path from business process with trade_phase_list - head_production_path_list = self.getHeadProductionPathList(transformation, - business_process) - factory = self.getFactory() - factory.product = dict( - resource=transformation.getResource(), - quantity=parent_movement.getNetQuantity(), - quantity_unit=parent_movement.getQuantityUnit(), - variation_category_list=parent_movement.getVariationCategoryList(), - variation_property_dict=parent_movement.getVariationPropertyDict(),) - - # consumed amounts are sorted by phase, but not ordered. - amount_dict = {} - for amount in transformation.getAggregatedAmountList(): - phase = amount.getTradePhase() - - if phase not in trade_phase_list: - raise TransformationRuleError,\ - "Trade phase %r is not part of Business Process %r" % (phase, business_process) - - amount_dict.setdefault(phase, []) - amount_dict[phase].append(amount) - - last_phase_path_list = list() # to keep phase_path_list - last_prop_dict = dict() - for (phase, amount_list) in amount_dict.items(): - phase_path_list = business_process.getPathValueList(phase) + # Simulation workflow + security.declareProtected(Permissions.ModifyPortalContent, 'expand') + def expand(self, applied_rule, **kw): """ - XXX: In this context, we assume quantity as ratio, - but this "quantity as ratio" is consistent with transformation. + Expands the current movement downward. + -> new status -> expanded + An applied rule can be expanded only if its parent movement + is expanded. """ - if sum(map(lambda path: path.getQuantity(), phase_path_list)) != 1: + parent_movement = applied_rule.getParentValue() + # Get production node and production section + production = parent_movement.getSource() + production_section = parent_movement.getSourceSection() + # Get the current supply link used to calculate consumed resource + # The current supply link is calculated from the parent AppliedRule. + supply_chain = self.getSupplyChain(parent_movement.getParentValue()) + parent_supply_link = self.getCurrentSupplyLink(parent_movement) + current_supply_link_list = supply_chain.\ + getPreviousProductionSupplyLinkList(parent_supply_link) + if len(current_supply_link_list) != 1: + # We shall no pass here. + # The test method returned a wrong value ! raise TransformationRuleError,\ - "sum ratio of Trade Phase %r of Business Process %r is not one"\ - % (phase, business_process) - - for path in phase_path_list: - # source, source_section - source_section = path.getSourceSection() # only support a static access - source_method_id = path.getSourceMethodId() - if source_method_id is None: - source = path.getSource() - else: - source = getattr(path, source_method_id)() - # destination, destination_section - destination_section = path.getDestinationSection() # only support a static access - destination_method_id = path.getDestinationMethodId() - if destination_method_id is None: - destination = path.getDestination() - else: - destination = getattr(path, destination_method_id)() - - start_date = path.getExpectedStartDate(explanation) - stop_date = path.getExpectedStopDate(explanation) - predecessor_remaining_phase_list = path.getPredecessorValue()\ - .getRemainingTradePhaseList(explanation, - trade_phase_list=trade_phase_list) - successor_remaining_phase_list = path.getSuccessorValue()\ - .getRemainingTradePhaseList(explanation, - trade_phase_list=trade_phase_list) - - # checking which is not last path of transformation - if len(successor_remaining_phase_list) != 0: - # partial produced movement - factory.requestProduced( - causality_value=path, - start_date=start_date, - stop_date=stop_date, - # when last path of transformation, path.getQuantity() will be return 1. - quantity=factory.product['quantity'] * path.getQuantity(), - source_section=source_section, - destination_section=destination_section, - destination=destination, - trade_phase_value_list=successor_remaining_phase_list) - else: - # for making movement of last product of the transformation - last_phase_path_list.append(path) - - # path params must be same - if last_prop_dict.get('start_date', None) is None: - last_prop_dict['start_date'] = start_date - if last_prop_dict.get('stop_date', None) is None: - last_prop_dict['stop_date'] = stop_date - # trade phase of product is must be empty [] - if last_prop_dict.get('trade_phase_value_list', None) is None: - last_prop_dict['trade_phase_value_list'] = successor_remaining_phase_list - if last_prop_dict.get('source_section', None) is None: - last_prop_dict['source_section'] = source_section - # for the source, it is not need, because the produced. - if last_prop_dict.get('destination_section', None) is None: - last_prop_dict['destination_section'] = destination_section - if last_prop_dict.get('destination', None) is None: - last_prop_dict['destination'] = destination - - if last_prop_dict['start_date'] != start_date or\ - last_prop_dict['stop_date'] != stop_date or\ - last_prop_dict['trade_phase_value_list'] != successor_remaining_phase_list or\ - last_prop_dict['source_section'] != source_section or\ - last_prop_dict['destination_section'] != destination_section or\ - last_prop_dict['destination'] != destination: - raise TransformationRuleError,\ - """Returned property is different on Transformation %r and Business Process %r"""\ - % (transformation, business_process) - - # when the path is part of production but not first, consume previous partial product - if path not in head_production_path_list: - factory.requestConsumed( - causality_value=path, - start_date=start_date, - stop_date=stop_date, - quantity=factory.product['quantity'] * path.getQuantity(), - source_section=source_section, - destination_section=destination_section, - source=source, - trade_phase_value_list=predecessor_remaining_phase_list) - - # consumed movement - for amount in amount_list: - factory.requestConsumed( - causality_value=path, - start_date=start_date, - stop_date=stop_date, - resource=amount.getResource(), - quantity=factory.product['quantity'] * amount.getQuantity()\ - / amount.getEfficiency() * path.getQuantity(), - quantity_unit=amount.getQuantityUnit(), - source_section=source_section, - destination_section=destination_section, - source=source, - trade_phase=path.getTradePhase()) + "Expand must not be called on %r" %\ + applied_rule.getRelativeUrl() + else: + current_supply_link = current_supply_link_list[0] + # Generate produced movement + movement_dict = self._expandProducedResource(applied_rule, + production, + production_section, + current_supply_link) + # Generate consumed movement + consumed_mvt_dict = self._expandConsumedResource(applied_rule, + production, + production_section, + current_supply_link) + movement_dict.update(consumed_mvt_dict) + # Finally, build movement + self._buildMovementList(applied_rule, movement_dict, **kw) + # Expand each movement created + Rule.expand(self, applied_rule, **kw) + + def _expandProducedResource(self, applied_rule, production, + production_section, current_supply_link): + """ + Produced resource. + Create a movement for the resource produced by the transformation. + Only one produced movement can be created. + """ + parent_movement = applied_rule.getParentValue() + stop_date = parent_movement.getStartDate() + produced_movement_dict = { + 'pr': { + "resource": parent_movement.getResource(), + # XXX what is lost quantity ? + "quantity": parent_movement.getQuantity(),# + lost_quantity, + "quantity_unit": parent_movement.getQuantityUnit(), + "variation_category_list":\ + parent_movement.getVariationCategoryList(), + "variation_property_dict": \ + parent_movement.getVariationPropertyDict(), + "source_list": (), + "source_section_list": (), + "destination": production, + "destination_section": production_section, + "deliverable": 1, + 'start_date': current_supply_link.calculateStartDate(stop_date), + 'stop_date': stop_date, + 'causality_value': current_supply_link, + } + } + return produced_movement_dict + + def _expandConsumedResource(self, applied_rule, production, + production_section, current_supply_link): + """ + Consumed resource. + Create a movement for each resource consumed by the transformation, + and for the previous variation of the produced resource. + """ + # Calculate all consumed resource + # Store each value in a dictionnary before created them. + # { movement_id: {property_name: property_value,} ,} + consumed_movement_dict = {} + parent_movement = applied_rule.getParentValue() + supply_chain = self.getSupplyChain(parent_movement.getParentValue()) + # Consumed previous variation + previous_variation_dict = self._expandConsumedPreviousVariation( + applied_rule, + production, + production_section, + supply_chain, + current_supply_link) + consumed_movement_dict.update(previous_variation_dict) + # Consumed raw materials + raw_material_dict = self._expandConsumedRawMaterials( + applied_rule, + production, + production_section, + supply_chain, + current_supply_link) + consumed_movement_dict.update(raw_material_dict) + return consumed_movement_dict + + def _expandConsumedPreviousVariation(self, applied_rule, production, + production_section, supply_chain, + current_supply_link): + """ + Create a movement for the previous variation of the produced resource. + """ + id_count = 1 + consumed_movement_dict = {} + parent_movement = applied_rule.getParentValue() + # Calculate the variation category list of parent movement + base_category_list = parent_movement.getVariationBaseCategoryList() + if "industrial_phase" in base_category_list: + # We do not want to get the industrial phase variation + base_category_list.remove("industrial_phase") + category_list = parent_movement.getVariationCategoryList( + base_category_list=base_category_list) + # Calculate the previous variation + for previous_supply_link in supply_chain.\ + getPreviousSupplyLinkList(current_supply_link): + previous_ind_phase_list = supply_chain.\ + getPreviousProductionIndustrialPhaseList(previous_supply_link, + all=1) + if previous_ind_phase_list != []: + # Industrial phase is a category + ind_phase_list = [x.getRelativeUrl() for x in \ + previous_ind_phase_list] + consumed_mvt_id = "%s_%s" % ("mr", id_count) + id_count += 1 + stop_date = parent_movement.getStartDate() + consumed_movement_dict[consumed_mvt_id] = { + 'start_date': current_supply_link.calculateStartDate(stop_date), + 'stop_date': stop_date, + "resource": parent_movement.getResource(), + # XXX Is the quantity value correct ? + "quantity": parent_movement.getNetQuantity(), # getNetQuantity to support efficency from transformation + "quantity_unit": parent_movement.getQuantityUnit(), + "destination_list": (), + "destination_section_list": (), + "source": production, + "source_section": production_section, + "deliverable": 1, + "variation_category_list": category_list+ind_phase_list, + "variation_property_dict": \ + parent_movement.getVariationPropertyDict(), + 'causality_value': current_supply_link, + } + return consumed_movement_dict + + def _expandConsumedRawMaterials(self, applied_rule, production, + production_section, supply_chain, + current_supply_link): + """ + Create a movement for each resource consumed by the transformation, + """ + parent_movement = applied_rule.getParentValue() + # Calculate the context for getAggregatedAmountList + base_category_list = parent_movement.getVariationBaseCategoryList() + if "industrial_phase" in base_category_list: + # We do not want to get the industrial phase variation + base_category_list.remove("industrial_phase") + category_list = parent_movement.getVariationCategoryList( + base_category_list=base_category_list) + # Get the transformation to use + transformation = self.getTransformation(applied_rule) + # Generate the fake context + tmp_context = parent_movement.asContext( + context=parent_movement, + REQUEST={'categories':category_list}) + # Calculate the industrial phase list + previous_ind_phase_list = supply_chain.\ + getPreviousPackingListIndustrialPhaseList(current_supply_link) + ind_phase_id_list = [x.getRelativeUrl() for x in previous_ind_phase_list] + # Call getAggregatedAmountList + # XXX expand failed if transformation is not defined. + # Do we need to catch the exception ? + amount_list = transformation.getAggregatedAmountList( + tmp_context, + ind_phase_url_list=ind_phase_id_list) + # Add entries in the consumed_movement_dict + consumed_movement_dict = {} + for amount in amount_list: + consumed_mvt_id = "%s_%s" % ("cr", amount.getId()) + stop_date = parent_movement.getStartDate() + resource_price = amount.getResourcePrice() + price = None + if resource_price is not None: + price = amount.getNetQuantity() * resource_price # getNetQuantity to support efficency from transformation + consumed_movement_dict[consumed_mvt_id] = { + 'start_date': current_supply_link.calculateStartDate(stop_date), + 'stop_date': stop_date, + "resource": amount.getResource(), + "variation_category_list":\ + amount.getVariationCategoryList(), + "variation_property_dict": \ + amount.getVariationPropertyDict(), + "quantity": amount.getNetQuantity() * parent_movement.getQuantity(), # getNetQuantity to support efficency from transformation + "price": price, + "quantity_unit": amount.getQuantityUnit(), + "destination_list": (), + "destination_section_list": (), + "source": production, + "source_section": production_section, + "deliverable": 1, + 'causality_value': current_supply_link, + } + return consumed_movement_dict + + security.declareProtected(Permissions.ModifyPortalContent, 'solve') + def solve(self, applied_rule, solution_list): + """ + Solve inconsistency according to a certain number of solutions + templates. This updates the - """ - valid graph for transformation - a --- b --- c + -> new status -> solved - a -- - \ - X b - / - c -- + This applies a solution to an applied rule. Once + the solution is applied, the parent movement is checked. + If it does not diverge, the rule is reexpanded. If not, + diverge is called on the parent movement. + """ - invalid graph - a ------- b - c ------- d + security.declareProtected(Permissions.ModifyPortalContent, 'diverge') + def diverge(self, applied_rule): + """ + -> new status -> diverged - -- b - / - a X - \ - -- c - """ - # when empty - if last_phase_path_list is None or len(last_phase_path_list) == 0: - raise TransformationRuleError,\ - """Could not make the product by Transformation %r and Business Process %r, -which last_phase_path_list is empty.""" % (transformation, business_process) + This basically sets the rule to "diverged" + and blocks expansion process + """ - factory.requestProduced( - causality_value_list=last_phase_path_list, - # when last path of transformation, path.getQuantity() will be return 1. - quantity=factory.product['quantity'] * path.getQuantity(), - **last_prop_dict) +# # Solvers +# security.declareProtected(Permissions.View, 'isDivergent') +# def isDivergent(self, applied_rule): +# """ +# Returns 1 if divergent rule +# """ +# +# security.declareProtected(Permissions.View, 'getDivergenceList') +# def getDivergenceList(self, applied_rule): +# """ +# Returns a list Divergence descriptors +# """ +# +# security.declareProtected(Permissions.View, 'getSolverList') +# def getSolverList(self, applied_rule): +# """ +# Returns a list Divergence solvers +# """ + + # Deliverability / orderability + def isDeliverable(self, m): + return 1 + def isOrderable(self, m): + return 0 - factory.makeMovements(applied_rule) - Rule.expand(self, applied_rule, **kw) diff --git a/product/ERP5/Document/TransformationSourcingRule.py b/product/ERP5/Document/TransformationSourcingRule.py index 20ddd7b431524417d9f41c694d8629a4c2ea2a1e..b8c3f815702385daabcd46beeef747be94ce31b9 100644 --- a/product/ERP5/Document/TransformationSourcingRule.py +++ b/product/ERP5/Document/TransformationSourcingRule.py @@ -27,113 +27,240 @@ # ############################################################################## +import ExtensionClass + from AccessControl import ClassSecurityInfo from Acquisition import aq_base, aq_parent, aq_inner, aq_acquire from Products.CMFCore.utils import getToolByName from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces from Products.ERP5.Document.Rule import Rule -from Products.ERP5.Document.TransformationRule import MovementFactory, TransformationRuleMixin from zLOG import LOG +class ProductionOrderError(Exception): pass class TransformationSourcingRuleError(Exception): pass -class SourcingMovementFactory(MovementFactory): - def __init__(self): - self.request_list = list() - - def requestSourcing(self, **sourcing): - self.request_list.append(sourcing) - - def getRequestList(self): - return self.request_list - -class TransformationSourcingRule(TransformationRuleMixin, Rule): +class TransformationSourcingRuleMixin(ExtensionClass.Base): """ - Transformation Sourcing Rule object make sure - items required in a Transformation are sourced + Mixin class used by TransformationSourcingRule and TransformationRule """ - # CMF Type Definition - meta_type = 'ERP5 Transformation Sourcing Rule' - portal_type = 'Transformation Sourcing Rule' # Declarative security security = ClassSecurityInfo() - security.declareObjectProtected(Permissions.AccessContentsInformation) - # Default Properties - property_sheets = ( PropertySheet.Base + security.declareProtected(Permissions.View, + 'getSupplyChain') + def getSupplyChain(self, applied_rule): + """ + Get the SupplyChain. + """ + # Get the SupplyChain to use + supply_chain_portal_type = "Supply Chain" + order = applied_rule.getRootAppliedRule().getCausalityValue() + supply_chain = order.getSpecialiseValue( + portal_type=supply_chain_portal_type) + if supply_chain is None: + raise ProductionOrderError,\ + "No SupplyChain defined on %s" % str(order) + else: + return supply_chain + + def getCurrentSupplyLink(self, movement): + """ + Get the current SupplyLink + """ + # Get the current supply link + supply_link_portal_type = "Supply Link" + current_supply_link = movement.getCausalityValue( + portal_type=supply_link_portal_type) + return current_supply_link + + security.declareProtected(Permissions.ModifyPortalContent, + '_buildMovementList') + def _buildMovementList(self, applied_rule, movement_dict,activate_kw=None,**kw): + """ + For each movement in the dictionnary, test if the movement already + exists. + If not, create it. + Then, update the movement attributes. + """ + for movement_id in movement_dict.keys(): + movement = applied_rule.get(movement_id) + # Create the movement if it does not exist + if movement is None: + movement = applied_rule.newContent( + portal_type=self.simulation_movement_portal_type, + id=movement_id, + activate_kw=activate_kw + ) + # We shouldn't modify frozen movements + elif movement.isFrozen(): + # FIXME: this is not perfect, instead of just skipping this one, we + # should generate a compensation movement + continue + # Update movement properties + movement.edit(activate_kw=activate_kw, **(movement_dict[movement_id])) + + security.declareProtected(Permissions.View, 'getTransformation') + def getTransformation(self, movement): + """ + Get transformation related to used by the applied rule. + """ + production_order_movement = movement.getRootSimulationMovement().\ + getOrderValue() + # XXX Acquisition can be use instead + parent_uid = production_order_movement.getParentUid() + explanation_uid = production_order_movement.getExplanationUid() + if parent_uid == explanation_uid: + production_order_line = production_order_movement + else: + production_order_line = production_order_movement.getParentValue() + script = production_order_line._getTypeBasedMethod('_getTransformation') + if script is not None: + transformation = script() + else: + line_transformation = production_order_line.objectValues( + portal_type=self.getPortalTransformationTypeList()) + if len(line_transformation)==1: + transformation = line_transformation[0] + else: + transformation = production_order_line.getSpecialiseValue( + portal_type=self.getPortalTransformationTypeList()) + return transformation + +class TransformationSourcingRule(TransformationSourcingRuleMixin, Rule): + """ + Transformation Sourcing Rule object make sure + items required in a Transformation are sourced + """ + # CMF Type Definition + meta_type = 'ERP5 Transformation Sourcing Rule' + portal_type = 'Transformation Sourcing Rule' + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + __implements__ = ( interfaces.IPredicate, + interfaces.IRule ) + # Default Properties + property_sheets = ( PropertySheet.Base , PropertySheet.XMLObject , PropertySheet.CategoryCore , PropertySheet.DublinCore , PropertySheet.Task ) - def getFactory(self): - return SourcingMovementFactory() + # Class variable + simulation_movement_portal_type = "Simulation Movement" - security.declareProtected(Permissions.ModifyPortalContent, 'expand') - def expand(self, applied_rule, **kw): - """ - Expands the current movement downward. - -> new status -> expanded - An applied rule can be expanded only if its parent movement - is expanded. - """ - parent_movement = applied_rule.getParentValue() - explanation = self.getExplanation(movement=parent_movement) - state = parent_movement.getCausalityValue().getPredecessorValue() - path_list = state.getSuccessorRelatedValueList() - - if len(path_list) == 0: - raise TransformationSourcingRuleError,\ - "Not found deliverable business path" - if len(path_list) > 1: - raise TransformationSourcingRuleError,\ - "Found 2 or more deliverable business path" - - path = path_list[0] - - # source, source_section - source_section = path.getSourceSection() # only support a static access - source_method_id = path.getSourceMethodId() - if source_method_id is None: - source = path.getSource() - else: - source = getattr(path, source_method_id)() - # destination, destination_section - destination_section = path.getDestinationSection() # only support a static access - destination_method_id = path.getDestinationMethodId() - if destination_method_id is None: - destination = path.getDestination() - else: - destination = getattr(path, destination_method_id)() - - start_date = path.getExpectedStartDate(explanation) - stop_date = path.getExpectedStopDate(explanation) - - quantity = parent_movement.getNetQuantity() * path.getQuantity() - price = parent_movement.getPrice() - if price is not None: - price *= path.getQuantity() - - factory = self.getFactory() - factory.requestSourcing( - causality_value=path, - source=source, - source_section=source_section, - destination=destination, - destination_section=destination_section, - resource=parent_movement.getResource(), - variation_category_list=parent_movement.getVariationCategoryList(), - variation_property_dict=parent_movement.getVariationPropertyDict(), - quantity=quantity, - price=price, - quantity_unit=parent_movement.getQuantityUnit(), - start_date=start_date, - stop_date=stop_date, - deliverable=1, - ) - - factory.makeMovements(applied_rule) - Rule.expand(self, applied_rule, **kw) + security.declareProtected(Permissions.ModifyPortalContent, 'expand') + def expand(self, applied_rule, activate_kw=None,**kw): + """ + Expands the current movement downward. + -> new status -> expanded + An applied rule can be expanded only if its parent movement + is expanded. + """ + parent_movement = applied_rule.getParentValue() + # Calculate the previous supply link + supply_chain = self.getSupplyChain(parent_movement.getParentValue()) + parent_supply_link = self.getCurrentSupplyLink(parent_movement) + previous_supply_link_list = supply_chain.\ + getPreviousPackingListSupplyLinkList( + parent_supply_link, + movement=parent_movement) + if len(previous_supply_link_list) == 0: + raise TransformationSourcingRuleError,\ + "Expand must not be called on %r" %\ + applied_rule.getRelativeUrl() + else: + movement_dict = {} + for previous_supply_link in previous_supply_link_list: + # Calculate the source + source_value = None + source_node = previous_supply_link.getSourceValue() + if source_node is not None: + source_value = source_node.getDestinationValue() + source_section_value = previous_supply_link.getSourceSectionValue() + # Generate the dict + stop_date = parent_movement.getStartDate() + movement_dict.update({ + "ts": { + 'source_value': source_value, + 'source_section_value': source_section_value, + 'destination_value': parent_movement.getSourceValue(), + 'destination_section_value': \ + parent_movement.getSourceSectionValue(), + 'resource_value': parent_movement.getResourceValue(), + 'variation_category_list': parent_movement.\ + getVariationCategoryList(), + "variation_property_dict": \ + parent_movement.getVariationPropertyDict(), + 'quantity': parent_movement.getNetQuantity(), # getNetQuantity to support efficency from transformation + 'price': parent_movement.getPrice(), + 'quantity_unit': parent_movement.getQuantityUnit(), + 'start_date': previous_supply_link.calculateStartDate(stop_date), + 'stop_date': stop_date, + 'deliverable': 1, + # Save the value of the current supply link + 'causality_value': previous_supply_link, + } + }) + # Build the movement + self._buildMovementList(applied_rule, movement_dict, + activate_kw=activate_kw) + # Create one submovement which sources the transformation + Rule.expand(self, applied_rule, activate_kw=activate_kw, **kw) + + security.declareProtected(Permissions.ModifyPortalContent, 'solve') + def solve(self, applied_rule, solution_list): + """ + Solve inconsistency according to a certain number of solutions + templates. This updates the + + -> new status -> solved + + This applies a solution to an applied rule. Once + the solution is applied, the parent movement is checked. + If it does not diverge, the rule is reexpanded. If not, + diverge is called on the parent movement. + """ + + security.declareProtected(Permissions.ModifyPortalContent, 'diverge') + def diverge(self, applied_rule): + """ + -> new status -> diverged + + This basically sets the rule to "diverged" + and blocks expansion process + """ + +# # Solvers +# security.declareProtected(Permissions.View, 'isDivergent') +# def isDivergent(self, applied_rule): +# """ +# Returns 1 if divergent rule +# """ +# +# security.declareProtected(Permissions.View, 'getDivergenceList') +# def getDivergenceList(self, applied_rule): +# """ +# Returns a list Divergence descriptors +# """ +# +# security.declareProtected(Permissions.View, 'getSolverList') +# def getSolverList(self, applied_rule): +# """ +# Returns a list Divergence solvers +# """ + + def isDeliverable(self, m): + resource = m.getResource() + if m.getResource() is None: + return 0 + if resource.find('operation/') >= 0: + return 0 + else: + return 1 + + def isOrderable(self, m): + return 0 diff --git a/product/ERP5/Document/TransformedResource.py b/product/ERP5/Document/TransformedResource.py index 866734d4a8f25d56b6c07b5c70a7ad24d17a71a9..dff8fbfaee98842a8dc8b5f7ce01f4117a57d3c3 100644 --- a/product/ERP5/Document/TransformedResource.py +++ b/product/ERP5/Document/TransformedResource.py @@ -322,7 +322,11 @@ class TransformedResource(Predicate, XMLObject, XMLMatrix, Amount): else: variation_category_list = self._getVariationCategoryList() variation_category_list_defined_by = self.getRelativeUrl() - trade_phase = self.getTradePhase() + if hasattr(self,"getTradePhase"): + # After installing BPM, trade_phase category to be exists + trade_phase = self.getTradePhase() + else: + trade_phase = None # Store values in Amount tmp_amount._edit( # Properties define on transformation line