def solve(self, simulation_movement): """ Split a simulation movement and accumulate """ movement_quantity = simulation_movement.getQuantity() delivery_quantity = simulation_movement.getDeliveryQuantity() new_movement_quantity = delivery_quantity * simulation_movement.getDeliveryRatio() applied_rule = simulation_movement.getParentValue() rule = applied_rule.getSpecialiseValue() # When accounting, the debit price is expressed by a minus quantity. # Thus, we must take into account the both minus and plus quantity. if ((movement_quantity < new_movement_quantity <= 0) or (movement_quantity > new_movement_quantity >= 0)): split_index = 0 new_id = "%s_split_%s" % (simulation_movement.getId(), split_index) while getattr(aq_base(applied_rule), new_id, None) is not None: split_index += 1 new_id = "%s_split_%s" % (simulation_movement.getId(), split_index) # Adopt different dates for deferred movements from erp5.component.module.MovementCollectionDiff import _getPropertyAndCategoryList movement_dict = _getPropertyAndCategoryList(simulation_movement) # new properties movement_dict.update( portal_type="Simulation Movement", id=new_id, quantity=movement_quantity - new_movement_quantity, activate_kw=self.activate_kw, delivery=None, **self.additional_parameters ) new_movement = applied_rule.newContent(**movement_dict) # Dirty code until IPropertyRecordable is revised. # Merge original simulation movement recorded property to new one. recorded_property_dict = simulation_movement._getRecordedPropertyDict(None) if recorded_property_dict: new_movement_recorded_property_dict = new_movement._getRecordedPropertyDict(None) if new_movement_recorded_property_dict is None: new_movement_recorded_property_dict = new_movement._recorded_property_dict = PersistentMapping() new_movement_recorded_property_dict.update(recorded_property_dict) # record zero quantity property, because this was originally zero. # without this, splitanddefer after accept decision does not work # properly. current_quantity = new_movement.getQuantity() new_movement.setQuantity(0) new_movement.recordProperty('quantity') new_movement.setQuantity(current_quantity) start_date = getattr(self, 'start_date', None) if start_date is not None: new_movement.recordProperty('start_date') new_movement.edit(start_date=start_date) stop_date = getattr(self, 'stop_date', None) if stop_date is not None: new_movement.recordProperty('stop_date') new_movement.edit(stop_date=stop_date) new_movement.expand(activate_kw=self.additional_parameters) # adopt new quantity on original simulation movement simulation_movement.edit(quantity=new_movement_quantity) simulation_movement.setDefaultActivateParameterDict(self.activate_kw) simulation_movement.expand(activate_kw=self.additional_parameters)
def updateMovementCollection(self, context, rounding=False, movement_generator=None): """ Invoke getMovementCollectionDiff and update context with the resulting IMovementCollectionDiff. context -- an IMovementCollection usually, possibly an IMovementList or an IMovement movement_generator -- an optional IMovementGenerator (if not specified, a context implicit IMovementGenerator will be used) """ movement_diff = self.getMovementCollectionDiff( context, rounding=rounding, movement_generator=movement_generator) # Apply Diff for movement in movement_diff.getDeletableMovementList(): movement.getParentValue().deleteContent(movement.getId()) for movement, kw in movement_diff.getUpdatableMovementList(): movement.edit(**kw) for property_id in kw: movement.clearRecordedProperty(property_id) for movement in movement_diff.getNewMovementList(): d = movement.__dict__ assert movement.isTempObject() if '_original' in d: # slow but safe way (required for compensated movements) context.newContent(portal_type=self.movement_type, **_getPropertyAndCategoryList(movement)) continue # fast way (we had to make sure such optimization # does not touch existing persistent data) del movement.__dict__ movement = context.newContent(portal_type=self.movement_type) d.update(movement.__dict__) categories = d.pop('categories') movement.__dict__ = d # Force update of local indexes on linked objects # (important for 'delivery'). movement._setCategoryList(categories)
def _solveBySplitting(self, activate_kw=None): """ contains all the logic to split. This method is convenient in case another solver needs it. """ solver_dict = {} new_movement_list = [] configuration_dict = self.getConfigurationPropertyDict() delivery_dict = {} for simulation_movement in self.getDeliveryValueList(): delivery_dict.setdefault(simulation_movement.getDeliveryValue(), []).append(simulation_movement) for movement, simulation_movement_list in delivery_dict.iteritems(): decision_quantity = movement.getQuantity() delivery_solver = self.getParentValue().newContent( portal_type=configuration_dict['delivery_solver'], temp_object=True) delivery_solver.setDeliveryValueList(simulation_movement_list) # Update the quantity using delivery solver algorithm split_list = delivery_solver.setTotalQuantity( decision_quantity, activate_kw=activate_kw) # Create split movements for (simulation_movement, split_quantity) in split_list: split_index = 0 simulation_id = simulation_movement.getId().split("_split_")[0] new_id = "%s_split_%s" % (simulation_id, split_index) applied_rule = simulation_movement.getParentValue() while getattr(aq_base(applied_rule), new_id, None) is not None: split_index += 1 new_id = "%s_split_%s" % (simulation_id, split_index) # Copy at same level kw = _getPropertyAndCategoryList(simulation_movement) kw.update(delivery=None, quantity=split_quantity) new_movement = applied_rule.newContent( new_id, simulation_movement.getPortalType(), activate_kw=activate_kw, **kw) new_movement_list.append(new_movement) # Dirty code until IPropertyRecordable is revised. # Merge original simulation movement recorded property to new one. recorded_property_dict = simulation_movement._getRecordedPropertyDict( None) if recorded_property_dict: new_movement_recorded_property_dict = new_movement._getRecordedPropertyDict( None) if new_movement_recorded_property_dict is None: new_movement_recorded_property_dict = new_movement._recorded_property_dict = PersistentMapping( ) new_movement_recorded_property_dict.update( recorded_property_dict) # record zero quantity property, because this was originally zero. # without this, splitanddefer after accept decision does not work # properly. current_quantity = new_movement.getQuantity() new_movement.setQuantity(0) new_movement.recordProperty('quantity') new_movement.setQuantity(current_quantity) start_date = configuration_dict.get('start_date', None) if start_date is not None: new_movement.recordProperty('start_date') new_movement.setStartDate(start_date) stop_date = configuration_dict.get('stop_date', None) if stop_date is not None: new_movement.recordProperty('stop_date') new_movement.setStopDate(stop_date) if activate_kw: new_movement.setDefaultActivateParameterDict({}) simulation_movement.expand(activate_kw=activate_kw) new_movement.expand(activate_kw=activate_kw) # Finish solving if self.getPortalObject().portal_workflow.isTransitionPossible( self, 'succeed'): self.succeed() solver_dict["new_movement_list"] = new_movement_list return solver_dict
def _solve(self, activate_kw=None): """This method create new movement based on difference of aggregate sets. It supports only removed items. Quantity divergence is also solved with sum of aggregated quantities stored on each updated movements. """ configuration_dict = self.getConfigurationPropertyDict() delivery_dict = {} portal = self.getPortalObject() for simulation_movement in self.getDeliveryValueList(): delivery_dict.setdefault(simulation_movement.getDeliveryValue(), []).append(simulation_movement) for movement, simulation_movement_list in delivery_dict.iteritems(): decision_aggregate_set = set(movement.getAggregateList()) split_list = [] for simulation_movement in simulation_movement_list: simulated_aggregate_set = set( simulation_movement.getAggregateList()) difference_set = simulated_aggregate_set.difference( decision_aggregate_set) mirror_difference_set = decision_aggregate_set.difference( simulated_aggregate_set) if difference_set: # There is less aggregates in prevision compare to decision split_list.append((simulation_movement, difference_set)) elif mirror_difference_set: # There is additional aggregates in decision compare to prevision raise NotImplementedError('Additional items detected. This solver'\ ' does not support such divergence resolution.') else: # Same set, no divergence continue # Create split movements for (simulation_movement, splitted_aggregate_set) in split_list: split_index = 0 new_id = "%s_split_%s" % (simulation_movement.getId(), split_index) applied_rule = simulation_movement.getParentValue() while getattr(aq_base(applied_rule), new_id, None) is not None: split_index += 1 new_id = "%s_split_%s" % (simulation_movement.getId(), split_index) # Copy at same level kw = _getPropertyAndCategoryList(simulation_movement) previous_aggregate_list = simulation_movement.getAggregateList( ) new_aggregate_list = list(set(previous_aggregate_list)\ .symmetric_difference(splitted_aggregate_set)) # freeze those properties only if not yet recorded # to avoid freezing already recorded value if not simulation_movement.isPropertyRecorded('aggregate'): simulation_movement.recordProperty('aggregate') # edit prevision movement simulation_movement.setAggregateList(new_aggregate_list) total_quantity = sum(item.getQuantity() for item in\ simulation_movement.getAggregateValueList()) simulation_movement.setQuantity(total_quantity) # create compensation decision movement total_quantity = sum([portal.restrictedTraverse(aggregate).getQuantity()\ for aggregate in splitted_aggregate_set]) kw.update({ 'portal_type': simulation_movement.getPortalType(), 'id': new_id, 'delivery': None }) # propagate same recorded properties from original movement # to store them in recorded_property for frozen_property in ( 'aggregate', 'start_date', 'stop_date', ): if simulation_movement.isPropertyRecorded(frozen_property): kw[frozen_property] = simulation_movement.getRecordedProperty( frozen_property) new_movement = applied_rule.newContent(activate_kw=activate_kw, **kw) # freeze aggregate property new_movement.recordProperty('aggregate') # edit compensation decision movement new_movement.setAggregateList(list(splitted_aggregate_set)) new_movement.setQuantity(total_quantity) if activate_kw is not None: new_movement.setDefaultActivateParameterDict(activate_kw) start_date = configuration_dict.get('start_date', None) if start_date is not None: new_movement.recordProperty('start_date') new_movement.setStartDate(start_date) stop_date = configuration_dict.get('stop_date', None) if stop_date is not None: new_movement.recordProperty('stop_date') new_movement.setStopDate(stop_date) # Finish solving if self.getPortalObject().portal_workflow.isTransitionPossible( self, 'succeed'): self.succeed()
def _getPropertyAndCategoryDict(self, explanation, amount, trade_model_path, delay_mode=None): """A private method to merge an amount and a business_link and return a dict of properties and categories which can be used to create a new movement. explanation -- an Order, Order Line, Delivery or Delivery Line or Applied Rule which implicitely defines a simulation subtree amount -- an IAmount instance or an IMovement instance trade_model_path -- an ITradeModelPath instance delay_mode -- optional value to specify calculation mode ('min', 'max') if no value specified use average delay """ if explanation.getPortalType() == "Applied Rule": rule = explanation.getSpecialiseValue() else: rule = None if rule is None: property_dict = _getPropertyAndCategoryList(amount) else: property_dict = {} for tester in rule._getUpdatingTesterList(): property_dict.update(tester.getUpdatablePropertyDict( amount, None)) # Arrow categories property_dict.update(trade_model_path.getArrowCategoryDict(context=amount)) # More categories for base_category in ('delivery_mode', 'incoterm', 'payment_mode', 'ledger'): value = trade_model_path.getPropertyList(base_category) if value: property_dict[base_category] = value # Amount quantities - XXX-JPS maybe we should consider handling unit conversions here # and specifying units if trade_model_path.getQuantity(): property_dict['quantity'] = trade_model_path.getQuantity() elif trade_model_path.getEfficiency(): property_dict['quantity'] = amount.getQuantity() *\ trade_model_path.getEfficiency() else: property_dict['quantity'] = amount.getQuantity() # Dates - the main concept of BPM is to search for reference dates # in parent simulation movements at expand time. This means that # a trade date which is based on a trade phase which is handled # by a child applied rule is not supported in ERP5 BPM. # In the same spirit, date calculation at expand time is local, not # global. if explanation.getPortalType() == 'Applied Rule': if explanation.getParentValue().getPortalType() != "Simulation Tool": # It only makes sense to search for start and stop dates for # applied rules which are not root applied rules. # Date calculation by Business Process can be also disabled by # leaving 'trade_date' unset (XXX: a separate boolean property, # on the TMP or the rule, may be better). if trade_model_path.getTradeDate(): property_dict['start_date'], property_dict['stop_date'] = \ self.getExpectedTradeModelPathStartAndStopDate( explanation, trade_model_path, delay_mode=delay_mode) # Else, nothing to do. This method can be used without Applied Rule. return property_dict