############################################################################## # # Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved. # Jean-Paul Smets-Solanes <jp@nexedi.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## from Products.CMFCore.utils import UniqueObject from AccessControl import ClassSecurityInfo from Globals import InitializeClass, DTMLFile from Products.ERP5Type.Document.Folder import Folder from Products.ERP5Type import Permissions from Products.ERP5Type.Tool.BaseTool import BaseTool from Products.ERP5 import _dtmldir from zLOG import LOG from Products.ERP5.Capacity.GLPK import solve from Numeric import zeros, resize from DateTime import DateTime # Solver Registration is_initialized = 0 delivery_solver_dict = {} delivery_solver_list = [] def registerDeliverySolver(solver): global delivery_solver_list, delivery_solver_dict #LOG('Register Solver', 0, str(solver.__name__)) delivery_solver_list.append(solver) delivery_solver_dict[solver.__name__] = solver target_solver_dict = {} target_solver_list = [] def registerTargetSolver(solver): global target_solver_list, target_solver_dict #LOG('Register Solver', 0, str(solver.__name__)) target_solver_list.append(solver) target_solver_dict[solver.__name__] = solver class Target: def __init__(self, **kw): """ Defines a target (target_quantity, start_date, stop_date) """ self.__dict__.update(kw) class SimulationTool (BaseTool): """ The SimulationTool implements the ERP5 simulation algorithmics. Examples of applications: - - ERP5 main purpose: - - """ id = 'portal_simulation' meta_type = 'ERP5 Simulation Tool' portal_type = 'Simulation Tool' allowed_types = ( 'ERP5 Applied Rule', ) # Declarative Security security = ClassSecurityInfo() # # ZMI methods # manage_options = ( ( { 'label' : 'Overview' , 'action' : 'manage_overview' } , ) + Folder.manage_options ) security.declareProtected( Permissions.ManagePortal, 'manage_overview' ) manage_overview = DTMLFile( 'explainSimulationTool', _dtmldir ) # Filter content (ZMI)) #def __init__(self): # return Folder.__init__(self, SimulationTool.id) # Filter content (ZMI)) def filtered_meta_types(self, user=None): # Filters the list of available meta types. all = SimulationTool.inheritedAttribute('filtered_meta_types')(self) meta_types = [] for meta_type in self.all_meta_types(): if meta_type['name'] in self.allowed_types: meta_types.append(meta_type) return meta_types def initialize(self): """ Update values of simulation movements based on delivery target values and solver """ from Products.ERP5.TargetSolver import Reduce, Defer, SplitAndDefer, CopyToTarget, Redirect from Products.ERP5.DeliverySolver import Distribute, Copy def isInitialized(self): global is_initialized return is_initialized def newDeliverySolver(self, solver_id, *args, **kw): """ Returns a solver instance """ if not self.isInitialized(): self.initialize() solver = delivery_solver_dict[solver_id](self, *args, **kw) return solver def applyDeliverySolver(self, movement, solver): """ Update values of simulation movements based on delivery target values and solver. movement -- a delivery line or cell solver -- a delivery solver """ if not self.isInitialized(): self.initialize() solver.solve(movement) def newTargetSolver(self, solver_id, *args, **kw): """ Returns a solver instance """ if not self.isInitialized(): self.initialize() solver = target_solver_dict[solver_id](self, *args, **kw) return solver def applyTargetSolver(self, movement, solver, new_target=None): """ Update upper targets based on new targets movement -- a simulation movement solver -- a target solver new_target-- new target values for that movement """ if new_target is None: # Default behaviour is to solve target based on # target defined by Delivery # it must be overriden in recursive upward update # to make sure new_target = Target(target_quantity = movement.getQuantity(), target_start_date = movement.getStartDate(), target_stop_date = movement.getStopDate(), target_destination = movement.getDestination(), target_destination_section = movement.getDestinationSection(), target_source = movement.getSource(), target_source_section = movement.getSourceSection()) if not self.isInitialized(): self.initialize() solver.solve(movement, new_target) def closeTargetSolver(self, solver): return solver.close() def showTargetSolver(self, solver): #LOG("SimulationTool",0,"in showTargetSolver") return str(solver.__dict__) ####################################################### # Stock Management security.declareProtected(Permissions.AccessContentsInformation, 'getInventory') def getInventory(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw) : """ from_date (>=) - to_date (<) - at_date (<=) - only take rows which date is <= at_date resource (only in generic API in simulation) node - only take rows in stock table which node_uid is equivalent to node payment - only take rows in stock table which payment_uid is equivalent to payment section - only take rows in stock table which section_uid is equivalent to section mirror_section resource_category - only take rows in stock table which resource_uid is in resource_category node_category - only take rows in stock table which node_uid is in section_category payment_category - only take rows in stock table which payment_uid is in section_category section_category - only take rows in stock table which section_uid is in section_category mirror_section_category variation_text - only take rows in stock table with specified variation_text this needs to be extended with some kind of variation_category ? XXX this way of implementing variation selection is far from perfect variation_category - variation or list of possible variations simulation_state - only take rows with specified simulation_state transit_simulation_state - take rows with specified transit_simulation_state and quantity < 0 omit_transit - do not evaluate transit_simulation_state input_simulation_state - only take rows with specified input_simulation_state and quantity > 0 output_simulation_state - only take rows with specified output_simulation_state and quantity < 0 ignore_variation - do not take into account variation in inventory calculation standardise - provide a standard quantity rather than an SKU omit_simulation omit_input omit_output selection_domain, selection_report - see ListBox **kw - if we want extended selection with more keywords (but bad performance) check what we can do with buildSqlQuery """ new_kw = {} new_kw.update(kw) sql_kw = {} date_dict = {'query':[], 'operator':'and'} if from_date : date_dict['query'].append(from_date) date_dict['range'] = 'min' if to_date : date_dict['query'].append(to_date) date_dict['range'] = 'minmax' elif to_date : date_dict['query'].append(to_date) date_dict['range'] = 'max' elif at_date : date_dict['query'].append(at_date) date_dict['range'] = 'ngt' if len(date_dict) : new_kw['stock.date'] = date_dict resource_uid_list = [] if type(resource) is type('') : resource_uid_list.append(self.portal_categories.restrictedTraverse(resource).getUid()) elif type(resource) is type([]) or type(resource) is type(()) : for resource_item in resource : resource_uid_list.append(self.portal_categories.restrictedTraverse(resource_item).getUid()) if len(resource_uid_list) : new_kw['stock.resource_uid'] = resource_uid_list node_uid_list = [] if type(node) is type('') : node_uid_list.append(self.portal_categories.restrictedTraverse(node).getUid()) elif type(node) is type([]) or type(node) is type(()) : for node_item in node : node_uid_list.append(self.portal_categories.restrictedTraverse(node_item).getUid()) if len(node_uid_list) : new_kw['stock.node_uid'] = node_uid_list payment_uid_list = [] if type(payment) is type('') : payment_uid_list.append(self.portal_categories.restrictedTraverse(payment).getUid()) elif type(payment) is type([]) or type(payment) is type(()) : for payment_item in payment : payment_uid_list.append(self.portal_categories.restrictedTraverse(payment_item).getUid()) if len(payment_uid_list) : new_kw['stock.payment_uid'] = payment_uid_list section_uid_list = [] if type(section) is type('') : section_uid_list.append(self.portal_categories.restrictedTraverse(section).getUid()) elif type(section) is type([]) or type(section) is type(()) : for section_item in section : section_uid_list.append(self.portal_categories.restrictedTraverse(section_item).getUid()) if len(section_uid_list) : new_kw['stock.section_uid'] = section_uid_list mirror_section_uid_list = [] if type(mirror_section) is type('') : mirror_section_uid_list.append(self.portal_categories.restrictedTraverse(mirror_section).getUid()) elif type(mirror_section) is type([]) or type(mirror_section) is type(()) : for mirror_section_item in mirror_section : mirror_section_uid_list.append(self.portal_categories.restrictedTraverse(mirror_section_item).getUid()) if len(mirror_section_uid_list) : new_kw['stock.mirror_section_uid'] = mirror_section_uid_list variation_text_list = [] if type(variation_text) is type('') : variation_text_list.append(variation_text) elif type(variation_text) is type([]) or type(variation_text) is type(()) : for variation_text_item in variation_text : variation_text_list.append(variation_text_item) if len(variation_text_list) : new_kw['stock.variation_text'] = variation_text_list resource_category_uid_list = [] if type(resource_category) is type('') : resource_category_uid_list.append(self.portal_categories.restrictedTraverse(resource_category).getUid()) elif type(resource_category) is type([]) or type(resource_category) is type(()) : for resource_category_item in resource_category : resource_category_uid_list.append(self.portal_categories.restrictedTraverse(resource_category_item).getUid()) if len(resource_category_uid_list) : new_kw['resourceCategory'] = resource_category_uid_list node_category_uid_list = [] if type(node_category) is type('') : node_category_uid_list.append(self.portal_categories.restrictedTraverse(node_category).getUid()) elif type(node_category) is type([]) or type(node_category) is type(()) : for node_category_item in node_category : node_category_uid_list.append(self.portal_categories.restrictedTraverse(node_category_item).getUid()) if len(node_category_uid_list) : new_kw['nodeCategory'] = node_category_uid_list payment_category_uid_list = [] if type(payment_category) is type('') : payment_category_uid_list.append(self.portal_categories.restrictedTraverse(payment_category).getUid()) elif type(payment_category) is type([]) or type(payment_category) is type(()) : for payment_category_item in payment_category : payment_category_uid_list.append(self.portal_categories.restrictedTraverse(payment_category_item).getUid()) if len(payment_category_uid_list) : new_kw['paymentCategory'] = payment_category_uid_list section_category_uid_list = [] if type(section_category) is type('') : section_category_uid_list.append(self.portal_categories.restrictedTraverse(section_category).getUid()) elif type(section_category) is type([]) or type(section_category) is type(()) : for section_category_item in section_category : section_category_uid_list.append(self.portal_categories.restrictedTraverse(section_category_item).getUid()) if len(section_category_uid_list) : new_kw['sectionCategory'] = section_category_uid_list mirror_section_category_uid_list = [] if type(mirror_section_category) is type('') : mirror_section_category_uid_list.append(self.portal_categories.restrictedTraverse(mirror_section_category).getUid()) elif type(mirror_section_category) is type([]) or type(mirror_section_category) is type(()) : for mirror_section_category_item in mirror_section_category : mirror_section_category_uid_list.append(self.portal_categories.restrictedTraverse(mirror_section_category_item).getUid()) if len(mirror_section_category_uid_list) : new_kw['mirrorSectionCategory'] = mirror_section_category_uid_list variation_category_uid_list = [] if type(variation_category) is type('') : variation_category_uid_list.append(self.portal_categories.restrictedTraverse(variation_category).getUid()) elif type(variation_category) is type([]) or type(variation_category) is type(()) : for variation_category_item in variation_category : variation_category_uid_list.append(self.portal_categories.restrictedTraverse(variation_category_item).getUid()) if len(variation_category_uid_list) : new_kw['variationCategory'] = variation_category_uid_list # Simulation States # first, we evaluate simulation_state if (type(simulation_state) is type('')) or (type(simulation_state) is type([])) or (type(simulation_state) is type(())) : if len(simulation_state) : sql_kw['input_simulation_state'] = simulation_state sql_kw['output_simulation_state'] = simulation_state # then, if omit_transit == 1, we evaluate (simulation_state - transit_simulation_state) for input_simulation_state if omit_transit == 1 : if (type(simulation_state) is type('')) or (type(simulation_state) is type([])) or (type(simulation_state) is type(())) : if len(simulation_state) : if (type(transit_simulation_state) is type('')) or (type(transit_simulation_state) is type([])) or (type(transit_simulation_state) is type(())) : if len(transit_simulation_state) : # when we know both are usable, we try to calculate (simulation_state - transit_simulation_state) if type(simulation_state) is type('') : simulation_state = [simulation_state] if type(transit_simulation_state) is type('') : transit_simulation_state = [transit_simulation_state] delivered_simulation_state_list = [] for state in simulation_state : if state not in transit_simulation_state : delivered_simulation_state_list.append(state) sql_kw['input_simulation_state'] = delivered_simulation_state_list # alternatively, the user can directly define input_simulation_state and output_simulation_state if (type(input_simulation_state) is type('')) or (type(input_simulation_state) is type([])) or (type(input_simulation_state) is type(())) : if len(input_simulation_state) : sql_kw['input_simulation_state'] = input_simulation_state if (type(output_simulation_state) is type('')) or (type(output_simulation_state) is type([])) or (type(output_simulation_state) is type(())) : if len(output_simulation_state) : sql_kw['output_simulation_state'] = output_simulation_state if type(sql_kw.get('input_simulation_state')) is type('') : sql_kw['input_simulation_state'] = [sql_kw['input_simulation_state']] if type(sql_kw.get('output_simulation_state')) is type('') : sql_kw['output_simulation_state'] = [sql_kw['output_simulation_state']] sql_kw.update(self.portal_catalog.buildSQLQuery(**new_kw)) if src__ : return self.Resource_zGetInventory(src__=1, ignore_variation=ignore_variation, standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, selection_report=selection_report, **sql_kw) result = self.Resource_zGetInventory(ignore_variation=ignore_variation, standardise=standardise, omit_simulation=omit_simulation, omit_input=omit_input, omit_output=omit_output, selection_domain=selection_domain, selection_report=selection_report, **sql_kw) if len(result) > 0: return result[0].inventory return 0.0 security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventory') def getCurrentInventory(self, **kw): """ Returns current inventory """ kw['simulation_state'] = self.getPortalCurrentInventoryStateList() return self.getInventory(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventory') def getAvailableInventory(self, **kw): """ Returns available inventory (current inventory - deliverable) """ kw['simulation_state'] = self.getPortalCurrentInventoryStateList() return self.getInventory(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventory') def getFutureInventory(self, **kw): """ Returns future inventory """ return self.getInventory(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryList') def getInventoryList(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns list of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_zGetInventoryList(resource_uid = [self.getUid()], resource=None, to_date=at_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state=simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryList') def getCurrentInventoryList(self, **kw): """ Returns list of current inventory grouped by section or site """ return self.getInventoryList(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryList') def getFutureInventoryList(self, **kw): """ Returns list of future inventory grouped by section or site """ return self.getInventoryList(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryStat') def getInventoryStat(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns statistics of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_zGetInventory(resource_uid = [self.getUid()], resource=None, to_date=at_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state=simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryStat') def getCurrentInventoryStat(self, **kw): """ Returns statistics of current inventory grouped by section or site """ return self.getInventoryStat(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryStat') def getFutureInventoryStat(self, **kw): """ Returns statistics of future inventory grouped by section or site """ return self.getInventoryStat(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryChart') def getInventoryChart(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns list of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_zGetInventoryChart(resource_uid = [self.getUid()], resource=None, to_date=at_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state=simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryChart') def getCurrentInventoryChart(self, **kw): """ Returns list of current inventory grouped by section or site """ return self.getInventoryChart(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryChart') def getFutureInventoryChart(self, **kw): """ Returns list of future inventory grouped by section or site """ return self.getInventoryChart(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryAssetPrice') def getInventoryAssetPrice(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns list of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_zGetInventoryAssetPrice(resource_uid = [self.getUid()], resource=None, to_date=at_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state=simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getCurrentInventoryAssetPrice') def getCurrentInventoryAssetPrice(self, **kw): """ Returns list of current inventory grouped by section or site """ return self.getInventoryAssetPrice(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getAvailableInventoryAssetPrice') def getAvailableInventoryAssetPrice(self, **kw): """ Returns list of available inventory grouped by section or site (current inventory - deliverable) """ return self.getInventoryAssetPrice(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getFutureInventoryAssetPrice') def getFutureInventoryAssetPrice(self, **kw): """ Returns list of future inventory grouped by section or site """ return self.getInventoryAssetPrice(**kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryHistoryList') def getInventoryHistoryList(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns list of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_getInventoryHistoryList( resource = self._getMovementResourceList(), from_date=from_date, to_date=to_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state = simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getInventoryHistoryChart') def getInventoryHistoryChart(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns list of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_getInventoryHistoryChart( resource = self._getMovementResourceList(), from_date=from_date, to_date=to_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state = simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getMovementHistoryList') def getMovementHistoryList(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns list of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_zGetMovementHistoryList( resource = self._getMovementResourceList(), from_date=from_date, to_date=to_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state = simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getMovementHistoryStat') def getMovementHistoryStat(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns statistics of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] return self.Resource_zGetInventory( resource = self._getMovementResourceList(), from_date=from_date, to_date=to_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state = simulation_state, **kw) security.declareProtected(Permissions.AccessContentsInformation, 'getNextNegativeInventoryDate') def getNextNegativeInventoryDate(self, from_date=None, to_date=None, at_date=None, resource=None, node=None, payment=None, section=None, mirror_section=None, resource_category=None, node_category=None, payment_category=None, section_category=None, mirror_section_category=None, simulation_state=None, transit_simulation_state = None, omit_transit=0, input_simulation_state = None, output_simulation_state=None, variation_text=None, variation_category=None, ignore_variation=0, standardise=0, omit_simulation=0, omit_input=0, omit_output=0, selection_domain=None, selection_report=None, src__=0, **kw): """ Returns statistics of inventory grouped by section or site """ if section_category is None: section_category = self.getPortalDefaultSectionCategory() if type(simulation_state) is type(''): simulation_state = [simulation_state] if from_date is None: from_date = DateTime() return self.Resource_getInventoryHistoryList( resource = self._getMovementResourceList(), from_date=from_date, to_date=to_date, section=section, node=node, payment=payment, node_category=node_category, section_category=section_category, payment_category=payment_category, simulation_state = simulation_state, **kw) ####################################################### # Movement Group Collection / Delivery Creation def collectMovement(self, movement_list,class_list=None,**kw): """ group movements in the way we want. Thanks to this method, we are able to retrieve movement classed by order, resource, criterion,.... movement_list : the list of movement wich we want to group check_list : the list of classes used to group movements. The order of the list is important and determines by what we will group movement first Typically, check_list is : [DateMovementGroup,PathMovementGroup,...] """ s_tool = self.portal_simulation from Products.ERP5.MovementGroup import OrderMovementGroup, PathMovementGroup from Products.ERP5.MovementGroup import DateMovementGroup, ResourceMovementGroup from Products.ERP5.MovementGroup import VariantMovementGroup, RootMovementGroup if class_list is None: # For compatibility reasons, by default we keep the previous order class_list = [OrderMovementGroup,PathMovementGroup,DateMovementGroup, ResourceMovementGroup,VariantMovementGroup] my_root_group = RootMovementGroup(class_list=class_list) for movement in movement_list: if not movement in my_root_group.movement_list : my_root_group.append(movement,class_list=class_list) return my_root_group def buildOrderList(self, movement_group): # Build orders from a list of movements (attached to orders) order_list = [] if movement_group is not None: for order_group in movement_group.group_list: if order_group.order is None: # Only build if there is not order yet for path_group in order_group.group_list : if path_group.destination.find('site/Stock_PF') >=0 : # Build a Production Order delivery_module = self.ordre_fabrication delivery_type = 'Production Order' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' else: # Build a Purchase Order delivery_module = self.commande_achat delivery_type = 'Purchase Order' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' # we create a new delivery for each DateGroup for date_group in path_group.group_list : for resource_group in date_group.group_list : # Create a new production Order for each resource (Modele) modele_url_items = resource_group.resource.split('/') modele_id = modele_url_items[len(modele_url_items)-1] try : modele_object = self.getPortalObject().modele[modele_id] except : modele_object = None if modele_object is not None : of_description = modele_id + ' ' + modele_object.getDefaultDestinationTitle('') else : of_description = modele_id new_delivery_id = str(delivery_module.generateNewId()) self.portal_types.constructContent(type_name = delivery_type, container = delivery_module, id = new_delivery_id, start_date = date_group.start_date, stop_date = date_group.stop_date, source = path_group.source, destination = path_group.destination, source_section = path_group.source_section, destination_section = path_group.destination_section, description = of_description, title = new_delivery_id ) delivery = delivery_module[new_delivery_id] # the new delivery is added to the order_list order_list.append(delivery) # Create each delivery_line in the new delivery new_delivery_line_id = str(delivery.generateNewId()) self.portal_types.constructContent(type_name = delivery_line_type, container = delivery, id = new_delivery_line_id, resource = resource_group.resource, ) delivery_line = delivery[new_delivery_line_id] line_variation_category_list = [] line_variation_base_category_dict = {} # compute line_variation_base_category_list and # line_variation_category_list for new delivery_line for variant_group in resource_group.group_list : for variation_item in variant_group.category_list : if not variation_item in line_variation_category_list : line_variation_category_list.append(variation_item) variation_base_category_items = variation_item.split('/') if len(variation_base_category_items) > 0 : line_variation_base_category_dict[variation_base_category_items[0]] = 1 # update variation_base_category_list and line_variation_category_list for delivery_line line_variation_base_category_list = line_variation_base_category_dict.keys() delivery_line.setVariationBaseCategoryList(line_variation_base_category_list) delivery_line.setVariationCategoryList(line_variation_category_list) # IMPORTANT : delivery cells are automatically created during setVariationCategoryList # update quantity for each delivery_cell for variant_group in resource_group.group_list : #LOG('Variant_group examin',0,str(variant_group.category_list)) object_to_update = None # if there is no variation of the resource, update delivery_line with quantities and price if len(variant_group.category_list) == 0 : object_to_update = delivery_line # else find which delivery_cell is represented by variant_group else : categories_identity = 0 #LOG('Before Check cell',0,str(delivery_cell_type)) #LOG('Before Check cell',0,str(delivery_line.contentValues())) for delivery_cell in delivery_line.contentValues(filter={'portal_type':'Delivery Cell'}) : #LOG('Check cell',0,str(delivery_cell)) #LOG('Check cell',0,str(variant_group.category_list)) #LOG('Check cell',0,str(delivery_cell.getVariationCategoryList())) if len(variant_group.category_list) == len(delivery_cell.getVariationCategoryList()) : #LOG('Parse category',0,str(delivery_cell.getVariationCategoryList())) for category in delivery_cell.getVariationCategoryList() : if not category in variant_group.category_list : #LOG('Not found category',0,str(category)) break else : categories_identity = 1 if categories_identity : object_to_update = delivery_cell break # compute quantity and price for delivery_cell or delivery_line and # build relation between simulation_movement and delivery_cell or delivery_line if object_to_update is not None : cell_quantity = 0 for movement in variant_group.movement_list : cell_quantity += movement.getConvertedQuantity() # We do not create a relation or modifu anything # since planification of this movement will create new applied rule object_to_update.edit(quantity = cell_quantity, force_update = 1) return order_list def buildDeliveryList(self, movement_group): # Build deliveries from a list of movements LOG('buildDeliveryList root_group',0,movement_group) LOG('buildDeliveryList root_group.__dict__',0,movement_group.__dict__) for group in movement_group.group_list: LOG('buildDeliveryList group.__dict__',0,group.__dict__) LOG('buildDeliveryList nested_class.__dict__',0,movement_group.nested_class.__dict__) def orderGroupProcessing(order_group, delivery_list, reindexable_movement_list, **kw): # Order should never be None LOG("buildDeliveryList", 0, str(order_group.__dict__)) if order_group.order is not None: order = self.portal_categories.resolveCategory(order_group.order) if order is not None: # define some variables LOG("order", 0, str(order.__dict__)) if order.getPortalType() == 'Purchase Order' : delivery_module = order.getPortalObject().livraison_achat delivery_type = 'Purchase Packing List' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' elif order.getPortalType() == 'Sale Order' : delivery_module = order.getPortalObject().sale_packing_list delivery_type = 'Sale Packing List' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' else : delivery_module = order.getPortalObject().livraison_vente delivery_type = 'Sales Packing List' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' else : # should never be none LOG("order is None", 0, str(order.__dict__)) return -1 else: # order is None order = None # possible when we build deliveries for tranfer of property delivery_module = self.getPortalObject().livraison_vente delivery_type = 'Sales Packing List' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' for path_group in order_group.group_list: pathGroupProcessing(path_group=path_group, delivery_module=delivery_module, delivery_type=delivery_type, delivery_line_type=delivery_line_type, delivery_cell_type=delivery_cell_type, order=order, delivery_list=delivery_list, reindexable_movement_list=reindexable_movement_list, **kw) return 0 def pathGroupProcessing(path_group, delivery_module, delivery_type, delivery_line_type, delivery_cell_type, order, delivery_list, reindexable_movement_list, default_rule_id=None, **kw): # we create a new delivery for each DateGroup if default_rule_id is 'default_amortisation_rule': pass else: # if path is internal ??? # JPS NEW if path_group.source is None or path_group.destination is None: # Production Path LOG("Builder",0, "Strange Path %s " % path_group.source) LOG("Builder",0, "Strange Path %s " % path_group.destination) LOG("Builder path_group in pathGroupProcessing",0, path_group.__dict__) if path_group.source is None or path_group.destination is None: pass #delivery_module = self.rapport_fabrication #delivery_type = 'Production Report' #delivery_line_type = 'Production Report Line' #delivery_cell_type = 'Production Report Cell' elif path_group.destination.find('site/Stock_PF') >= 0 and \ path_group.source.find('site/Piquage') >= 0: delivery_module = self.livraison_fabrication delivery_type = 'Production Packing List' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' elif path_group.source.find('site/Stock_MP') >= 0 and \ path_group.destination.find('site/Piquage') >= 0: delivery_module = self.livraison_fabrication delivery_type = 'Production Packing List' delivery_line_type = delivery_type + ' Line' delivery_cell_type = 'Delivery Cell' for date_group in path_group.group_list : dateGroupProcessing(date_group=date_group, path_group=path_group, delivery_module=delivery_module, delivery_type=delivery_type, delivery_line_type=delivery_line_type, delivery_cell_type=delivery_cell_type, order=order, delivery_list=delivery_list, reindexable_movement_list=reindexable_movement_list, default_rule_id=default_rule_id, **kw) def dateGroupProcessing(date_group, path_group, delivery_module, delivery_type, delivery_line_type, delivery_cell_type, order, delivery_list, reindexable_movement_list, default_rule_id=None, resource=None, **kw): if default_rule_id == 'default_amortisation_rule': accounting_transaction_data_list = {} for path_group in date_group.group_list: source_section = path_group.source_section destination_section = path_group.destination_section source = path_group.source destination = path_group.destination accounting_transaction_data = accounting_transaction_data_list.get( (source_section, destination_section), None) if accounting_transaction_data is None: accounting_transaction_data_list[ (source_section, destination_section) ] = {} accounting_transaction_data = accounting_transaction_data_list.get( (source_section, destination_section), None) quantity = 0 source_movement_list = [] for movement in path_group.movement_list: if movement.getDeliveryValue() is None: quantity += movement.getQuantity() source_movement_list.append(movement) accounting_transaction_data[ (source, destination) ] = (quantity, source_movement_list) if len(source_movement_list) == 0: del accounting_transaction_data[ (source, destination) ] for (source_section, destination_section), accounting_transaction_data in accounting_transaction_data_list.items(): if len(accounting_transaction_data.items()) > 0: new_delivery_id = str(delivery_module.generateNewId()) accounting_transaction = delivery_module.newContent(portal_type = delivery_type, id = new_delivery_id, start_date = date_group.start_date, stop_date = date_group.stop_date, source_section = source_section, destination_section = destination_section ) accounting_transaction.setResource(resource) for (source, destination), (quantity, source_movement_list) in accounting_transaction_data.items(): new_transaction_line_id = str(accounting_transaction.generateNewId()) accounting_transaction_line = accounting_transaction.newContent(type_name = delivery_line_type, id = new_transaction_line_id, source = source, destination = destination) accounting_transaction_line = accounting_transaction[new_transaction_line_id] accounting_transaction_line.setQuantity(quantity) accounting_transaction_line.setResource(resource) for movement in source_movement_list: movement.setDeliveryValue(accounting_transaction_line) movement.recursiveImmediateReindexObject() else: # Create a new packing list new_delivery_id = str(delivery_module.generateNewId()) delivery = delivery_module.newContent(type_name = delivery_type, id = new_delivery_id, start_date = date_group.start_date, stop_date = date_group.stop_date, source = path_group.source, destination = path_group.destination, source_section = path_group.source_section, destination_section = path_group.destination_section, ) if order is not None : delivery.edit(title = order.getTitle(), causality_value = order, incoterm = order.getIncoterm(), delivery_mode = order.getDeliveryMode() ) # the new delivery is added to the delivery_list delivery_list.append(delivery) # LOG('Livraison cr��e',0,str(delivery.getId())) # Create each delivery_line in the new delivery for resource_group in date_group.group_list : resourceGroupProcessing(resource_group=resource_group, delivery=delivery, delivery_type=delivery_type, delivery_line_type=delivery_line_type, delivery_cell_type=delivery_cell_type, delivery_list=delivery_list, reindexable_movement_list=reindexable_movement_list, **kw) def resourceGroupProcessing(resource_group, delivery, delivery_type, delivery_line_type, delivery_cell_type, delivery_list, reindexable_movement_list, delivery_module=None, default_rule_id=None, **kw): if default_rule_id == 'default_amortisation_rule': resource = resource_group.resource for date_group in resource_group.group_list: dateGroupProcessing(date_group=date_group, path_group=None, delivery_module=delivery_module, delivery_type=delivery_type, delivery_line_type=delivery_line_type, delivery_cell_type=delivery_cell_type, order=None, delivery_list=delivery_list, reindexable_movement_list=reindexable_movement_list, default_rule_id=default_rule_id, resource=resource) else: if delivery_type == 'Production Report': if resource_group.resource.find('operation') == 0: delivery_line_type = 'Production Report Operation' else: delivery_line_type = 'Production Report Component' #new_delivery_line_id = str(delivery.generateNewId()) delivery_line = delivery.newContent(type_name = delivery_line_type, resource = resource_group.resource, ) line_variation_category_list = [] line_variation_base_category_dict = {} # compute line_variation_base_category_list and # line_variation_category_list for new delivery_line for variant_group in resource_group.group_list : for variation_item in variant_group.category_list : if not variation_item in line_variation_category_list : line_variation_category_list.append(variation_item) variation_base_category_items = variation_item.split('/') if len(variation_base_category_items) > 0 : line_variation_base_category_dict[variation_base_category_items[0]] = 1 # update variation_base_category_list and line_variation_category_list for delivery_line line_variation_base_category_list = line_variation_base_category_dict.keys() delivery_line._setVariationBaseCategoryList(line_variation_base_category_list) delivery_line.setVariationCategoryList(line_variation_category_list) # IMPORTANT : delivery cells are automatically created during setVariationCategoryList # update quantity for each delivery_cell for variant_group in resource_group.group_list: #LOG('Variant_group examin?,0,str(variant_group.category_list)) object_to_update = None # if there is no variation of the resource, update delivery_line with quantities and price if len(variant_group.category_list) == 0 : object_to_update = delivery_line # else find which delivery_cell is represented by variant_group else : categories_identity = 0 #LOG('Before Check cell',0,str(delivery_cell_type)) #LOG('Before Check cell',0,str(delivery_line.contentValues())) for delivery_cell in delivery_line.contentValues( filter={'portal_type':delivery_cell_type}) : #LOG('Check cell',0,str(delivery_cell)) if len(variant_group.category_list) == len(delivery_cell.getVariationCategoryList()) : #LOG('Parse category',0,str(delivery_cell.getVariationCategoryList())) for category in delivery_cell.getVariationCategoryList() : if not category in variant_group.category_list : #LOG('Not found category',0,str(category)) break else : categories_identity = 1 if categories_identity : object_to_update = delivery_cell break # compute quantity, quantity and price for delivery_cell or delivery_line and # build relation between simulation_movement and delivery_cell or delivery_line if object_to_update is not None : cell_quantity = 0 cell_total_price = 0 for movement in variant_group.movement_list : LOG('SimulationTool, movement.getPhysicalPath',0,movement.getPhysicalPath()) LOG('SimulationTool, movement.showDict',0,movement.showDict()) cell_quantity += movement.getNetConvertedQuantity() try: cell_total_price += movement.getNetConvertedQuantity()*movement.getPrice() # XXX WARNING - ADD PRICED QUANTITY except: cell_total_price = None if movement.getPortalType() == 'Simulation Movement' : # update every simulation_movement # we set delivery_value and target dates and quantity movement._setDeliveryValue(object_to_update) movement._setQuantity(movement.getQuantity()) movement._setEfficiency(movement.getEfficiency()) movement._setStartDate(movement.getStartDate()) movement._setStopDate(movement.getStopDate()) movement._setSource(movement.getSource()) movement._setDestination(movement.getDestination()) movement._setSourceSection(movement.getSourceSection()) movement._setDestinationSection(movement.getDestinationSection()) # We will reindex later reindexable_movement_list.append(movement) if cell_quantity <> 0 and cell_total_price is not None: average_price = cell_total_price/cell_quantity else : average_price = 0 #LOG('object mis � jour',0,str(object_to_update.getRelativeUrl())) object_to_update._edit(quantity = cell_quantity, price = average_price, force_update = 1, ) delivery_list = [] reindexable_movement_list = [] if movement_group is not None: # Verify the rule used to build the movements default_rule_id = None if len(movement_group.movement_list) > 0: f = getattr(movement_group.movement_list[0], 'getRootAppliedRule', None) if f is not None: applied_rule = f() default_rule_id = applied_rule.getSpecialiseId() if default_rule_id == 'default_amortisation_rule': delivery_module = self.getPortalObject().accounting delivery_type = 'Amortisation Transaction' delivery_line_type = delivery_type + ' Line' delivery_cell_type = None for resource_group in movement_group.group_list: resourceGroupProcessing(resource_group=resource_group, delivery=None, delivery_module=delivery_module, delivery_type=delivery_type, delivery_line_type=delivery_line_type, delivery_cell_type=delivery_cell_type, delivery_list=delivery_list, reindexable_movement_list=reindexable_movement_list, default_rule_id=default_rule_id) for movement in movement_group.movement_list: movement.immediateReindexObject() else: for order_group in movement_group.group_list: if orderGroupProcessing(order_group=order_group, delivery_list=delivery_list, reindexable_movement_list=reindexable_movement_list) == -1: return delivery_list # If we reach this point, it means we could # create deliveries # get_transaction().commit() # DO NOT USE COMMIT BECAUSE OF WORKFLOW # Now, let us index what must be indexed # Since we comitted changes, there should be no risk of conflict LOG('reindexable_movement_list',0,reindexable_movement_list) for movement in reindexable_movement_list: LOG('will reindex this object: ',0,movement) # We have to use 'immediate' to bypass the activity tool, # because we will depend on these objects when we try to call buildInvoiceList # movement.reindexObject() # we do it now because we need to movement.immediateReindexObject() # we do it now because we need to # update category relation # Now return deliveries which were created return delivery_list ####################################################### # Capacity Management security.declareProtected( Permissions.ModifyPortalContent, 'updateCapacity' ) def updateCapacity(self, node): capacity_item_list = [] for o in node.contentValues(): if o.isCapacity(): # Do whatever is needed capacity_item_list += o.asCapacityItemList() pass # Do whatever with capacity_item_list # and store the resulting new capacity in node node._capacity_item_list = capacity_item_list security.declareProtected( Permissions.ModifyPortalContent, 'isMovementInsideCapacity' ) def isMovementInsideCapacity(self, movement): """ Purpose: provide answer to customer for the question "can you do it ?" movement: date source destination (2 nodes) source_section ... """ # Get nodes and dat source_node = movement.getSourceValue() destination_node = movement.getDestinationValue() start_date = movement.getStartDate() stop_date = movement.getStopDate() # Return result return self.isNodeInsideCapacity(source_node, start_date, additional_movement=movement, sign=1) and self.isNodeInsideCapacity(destination_node, stop_date, additional_movement=movement, sign=-1) security.declareProtected( Permissions.ModifyPortalContent, 'isNodeInsideCapacity' ) def isNodeInsideCapacity(self, node, date, simulation_state=None, additional_movement=None, sign=1): """ Purpose: decide if a node is consistent with its capacity definitions at a certain date (ie. considreing the stock / production history """ # First get the current inventory situation for this node inventory_list = node.getInventoryList(XXXXX) # Add additional movement if additional_movement: inventory_list = inventory_list + sign * additional_movement # needs to be implemented # Return answer return self.isAmountListInsideCapacity(node, inventory_list) security.declareProtected( Permissions.ModifyPortalContent, 'isAmountListInsideCapacity' ) def isAmountListInsideCapacity(self, node, amount_list, resource_aggregation_base_category=None, resource_aggregation_depth=None): """ Purpose: decide if a list of amounts is consistent with the capacity of a node If any resource in amount_list is missing in the capacity of the node, resource aggregation is performed, based on resource_aggregation_base_category. If the base category is not specified, it is an error (should guess instead?). The resource aggregation is done at the level of resource_aggregation_depth in the tree of categories. If resource_aggregation_depth is not specified, it's an error. Assumptions: amount_list is an association list, like ((R1 V1) (R2 V2)). node has an attribute '_capacity_item_list' which is a list of association lists. resource_aggregation_base_category is a Base Category object or a list of Base Category objects or None. resource_aggregation_depth is a strictly positive integer or None. """ # Make a copy of the attribute _capacity_item_list, because it may be necessary # to modify it for resource aggregation. capacity_item_list = node._capacity_item_list[:] # Make a mapping between resources and its indices. resource_map = {} index = 0 for alist in capacity_item_list: for pair in alist: resource = pair[0] # LOG('isAmountListInsideCapacity', 0, # "resource is %s" % repr(resource)) if resource not in resource_map: resource_map[resource] = index index += 1 # Build a point from the amount list. point = zeros(index, 'd') # Fill up zeros for safety. mask_map = {} # This is used to skip items in amount_list. for amount in amount_list: if amount[0] in mask_map: continue # This will fail, if amount_list has any different resource from the capacity. # If it has any different point, then we should ...... # # There would be two possible different solutions: # 1) If a missing resource is a meta-resource of resources supported by the capacity, # it is possible to add the resource into the capacity by aggregation. # 2) If a missing resource has a meta-resource as a parent and the capacity supports # the meta-resource directly or indirectly (`indirectly' means `by aggregation'), # it is possible to convert the missing resource into the meta-resource. # # However, another way has been implemented here. This does the following, if the resource # is not present in the capacity: # 1) If the value is zero, just ignore the resource, because zero is always acceptable. # 2) Attempt to aggregate resources both of the capacity and of the amount list. This aggregation # is performed at the depth of 'resource_aggregation_depth' under the base category # 'resource_aggregation_base_category'. # resource = amount[0] if resource in resource_map: point[resource_map[amount[0]]] = amount[1] else: if amount[1] == 0: # If the value is zero, no need to consider. pass elif resource_aggregation_base_category is None or resource_aggregation_depth is None: # XXX use an appropriate error class # XXX should guess a base category instead of emitting an exception raise RuntimeError, "The resource '%s' is not found in the capacity, and the argument 'resource_aggregation_base_category' or the argument 'resource_aggregation_depth' is not specified" % resource else: # It is necessary to aggregate resources, to guess the capacity of this resource. def getAggregationResourceUrl(url, depth): # Return a partial url of the argument 'url'. # If 'url' is '/foo/bar/baz' and 'depth' is 2, return '/foo/bar'. pos = 0 for i in range(resource_aggregation_depth): pos = url.find('/', pos+1) if pos < 0: break if pos < 0: return None pos = url.find('/', pos+1) if pos < 0: pos = len(url) return url[:pos] def getAggregatedResourceList(aggregation_url, category, resource_list): # Return a list of resources which should be aggregated. 'aggregation_url' is used # for a top url of those resources. 'category' is a base category for the aggregation. aggregated_resource_list = [] for resource in resource_list: for url in resource.getCategoryMembershipList(category, base=1): if url.startswith(aggregation_url): aggregated_resource_list.append(resource) return aggregated_resource_list def getAggregatedItemList(item_list, resource_list, aggregation_resource): # Return a list of association lists, which is a result of an aggregation. # 'resource_list' is a list of resources which should be aggregated. # 'aggregation_resource' is a category object which is a new resource created by # this aggregation. # 'item_list' is a list of association lists. new_item_list = [] for alist in item_list: new_val = 0 new_alist = [] # If a resource is not a aggregated, then add it to the new alist as it is. # Otherwise, aggregate it to a single value. for pair in alist: if pair[0] in resource_list: new_val += pair[1] else: new_alist.append(pair) # If it is zero, ignore this alist, as it is nonsense. if new_val != 0: new_alist.append([aggregation_resource, new_val]) new_item_list.append(new_alist) return new_item_list # Convert this to a string if necessary, for convenience. if type(resource_aggregation_base_category) not in (type([]), type(())): resource_aggregation_base_category = (resource_aggregation_base_category,) done = 0 # LOG('isAmountListInsideCapacity', 0, # "resource_aggregation_base_category is %s" % repr(resource_aggregation_base_category)) for category in resource_aggregation_base_category: for resource_url in resource.getCategoryMembershipList(category, base=1): aggregation_url = getAggregationResourceUrl(resource_url, resource_aggregation_depth) if aggregation_url is None: continue aggregated_resource_list = getAggregatedResourceList (aggregation_url, category, resource_map.keys()) # If any, do the aggregation. if len(aggregated_resource_list) > 0: aggregation_resource = self.portal_categories.resolveCategory(aggregation_url) # Add the resource to the mapping. # LOG('aggregation_resource', 0, str(aggregation_resource)) resource_map[aggregation_resource] = index index += 1 # Add the resource to the point. point = resize(point, (index,)) val = 0 for aggregated_amount in amount_list: for url in aggregated_amount[0].getCategoryMembershipList(category, base=1): if url.startswith(aggregation_url): val += aggregated_amount[1] mask_map[aggregated_amount[0]] = None break point[index-1] = val # Add capacity definitions of the resource into the capacity. capacity_item_list += getAggregatedItemList(capacity_item_list, aggregated_resource_list, aggregation_resource) done = 1 break if done: break if not done: raise RuntimeError, "Aggregation failed" # Build a matrix from the capacity item list. # LOG('resource_map', 0, str(resource_map)) matrix = zeros((len(capacity_item_list)+1, index), 'd') for index in range(len(capacity_item_list)): for pair in capacity_item_list[index]: matrix[index,resource_map[pair[0]]] = pair[1] # LOG('isAmountListInsideCapacity', 0, # "matrix = %s, point = %s, capacity_item_list = %s" % (str(matrix), str(point), str(capacity_item_list))) return solve(matrix, point) # Asset Price Calculation def updateAssetPrice(self, resource, variation_text, section_category, node_category, strict_membership=0, simulation_state=None): if simulation_state is None: simulation_state = self.getPortalCurrentInventoryStateList() section_value = self.portal_categories.resolveCategory(section_category) node_value = self.portal_categories.resolveCategory(node_category) # Initialize price current_asset_price = 0.0 # Missing: initial inventory price !!! current_inventory = 0.0 # Parse each movement brain_list = self.Resource_zGetMovementHistoryList(resource=[resource], variation_text=variation_text, section_category=section_category, node_category=node_category, strict_membership=strict_membership, simulation_state=simulation_state) # strict_membership not taken into account # We select movements related to certain nodes (ex. Stock) and sections (ex.Coramy Group) result = [] for b in brain_list: m = b.getObject() if m is not None: previous_inventory = current_inventory inventory_quantity = b.quantity # We should use the aggregated quantity provided by Resource_zGetMovementHistoryList quantity = m.getQuantity() # The movement quantity is important to determine the meaning of source and destination # Maybe we should take care of target qty in delired deliveries if quantity is None: quantity = 0.0 if m.getSourceValue() is None: # This is a production movement or an inventory movement # Use Industrial Price current_inventory += inventory_quantity # Update inventory if m.getPortalType() in ('Inventory Line', 'Inventory Cell'): # XX should be replaced by isInventory ??? asset_price = m.getPrice() if asset_price in (0.0, None): asset_price = current_asset_price # Use current price if no price defined else: # this is a production asset_price = m.getIndustrialPrice() if asset_price is None: asset_price = current_asset_price # Use current price if no price defined result.append((m.getRelativeUrl(), m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Production or Inventory', 'Price: %s' % asset_price )) elif m.getDestinationValue() is None: # This is a consumption movement or an inventory movement current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Consumption or Inventory', 'Price: %s' % asset_price )) elif m.getSourceValue().isAcquiredMemberOf(node_category) and m.getDestinationValue().isAcquiredMemberOf(node_category): # This is an internal movement current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Internal', 'Price: %s' % asset_price )) elif m.getSourceValue().isAcquiredMemberOf(node_category) and quantity < 0: # This is a physically inbound movement - try to use commercial price if m.getSourceSectionValue() is None: # No meaning current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Error', 'Price: %s' % asset_price )) elif m.getDestinationSectionValue() is None: # No meaning current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Error', 'Price: %s' % asset_price )) elif m.getDestinationSectionValue().isAcquiredMemberOf(section_category): current_inventory += inventory_quantity # Update inventory if m.getDestinationValue().isAcquiredMemberOf('site/Piquage'): # Production asset_price = m.getIndustrialPrice() if asset_price is None: asset_price = current_asset_price # Use current price if no price defined result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Production', 'Price: %s' % asset_price )) else: # Inbound from same section asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Inbound same section', 'Price: %s' % asset_price )) else: current_inventory += inventory_quantity # Update inventory asset_price = m.getPrice() result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Inbound different section', 'Price: %s' % asset_price )) elif m.getDestinationValue().isAcquiredMemberOf(node_category) and quantity > 0: # This is a physically inbound movement - try to use commercial price if m.getSourceSectionValue() is None: # No meaning current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Error', 'Price: %s' % asset_price )) elif m.getDestinationSectionValue() is None: # No meaning current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Error', 'Price: %s' % asset_price )) elif m.getSourceSectionValue().isAcquiredMemberOf(section_category): current_inventory += inventory_quantity # Update inventory if m.getSourceValue().isAcquiredMemberOf('site/Piquage'): # Production asset_price = m.getIndustrialPrice() if asset_price is None: asset_price = current_asset_price # Use current price if no price defined result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Production', 'Price: %s' % asset_price )) else: # Inbound from same section asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Inbound same section', 'Price: %s' % asset_price )) else: current_inventory += inventory_quantity # Update inventory asset_price = m.getPrice() result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Inbound different section', 'Price: %s' % asset_price )) else: # Outbound movement current_inventory += inventory_quantity # Update inventory asset_price = current_asset_price result.append((m.getRelativeUrl(),m.getStartDate(), m.getSource(), m.getSourceSection(), m.getDestination(), m.getDestinationSection(), m.getQuantity(), 'Outbound', 'Price: %s' % asset_price )) # Update asset_price if current_inventory > 0: if inventory_quantity is not None: # Update price with an average of incoming goods and current goods current_asset_price = ( current_asset_price * previous_inventory + asset_price * inventory_quantity ) / float(current_inventory) else: # New price is the price of incoming goods - negative stock has no meaning for asset calculation current_asset_price = asset_price result.append(('###New Asset Price', current_asset_price, 'New Inventory', current_inventory)) # Update Asset Price on the right side if m.getSourceSectionValue() is not None and m.getSourceSectionValue().isAcquiredMemberOf(section_category): # for each movement, source section is member of one and one only accounting category # therefore there is only one and one only source asset price m._setSourceAssetPrice(current_asset_price) #quantity = m.getInventoriatedQuantity() #if quantity: # #total_asset_price = - current_asset_price * quantity # #m.Movement_zSetSourceTotalAssetPrice(uid=m.getUid(), total_asset_price = total_asset_price) # m._setSourceAssetPrice(current_asset_price) if m.getDestinationSectionValue() is not None and m.getDestinationSectionValue().isMemberOf(section_category): # for each movement, destination section is member of one and one only accounting category # therefore there is only one and one only destination asset price m._setDestinationAssetPrice(current_asset_price) #quantity = m.getInventoriatedQuantity() #if quantity: # total_asset_price = current_asset_price * quantity # m.Movement_zSetDestinationTotalAssetPrice(uid=m.getUid(), total_asset_price = total_asset_price) # Global reindexing required afterwards in any case: so let us do it now # Until we get faster methods (->reindexObject()) #m.immediateReindexObject() m.reindexObject() #m.activate(priority=7).immediateReindexObject() # Too slow return result # Used for mergeDeliveryList. class MergeDeliveryListError(Exception): pass security.declareProtected( Permissions.ModifyPortalContent, 'mergeDeliveryList' ) def mergeDeliveryList(self, delivery_list): """ Merge multiple deliveries into one delivery. All delivery lines are merged into the first one. The first one is therefore called main_delivery here. The others are cancelled. Return the main delivery. """ # Sanity checks. if len(delivery_list) == 0: raise self.MergeDeliveryListError, "No delivery is passed" elif len(delivery_list) == 1: raise self.MergeDeliveryListError, "Only one delivery is passed" main_delivery = delivery_list[0] delivery_list = delivery_list[1:] # Another sanity check. It is necessary for them to be identical in some attributes. for delivery in delivery_list: for attr in ('portal_type', 'simulation_state', 'source', 'destination', 'source_section', 'destination_section', 'source_decision', 'destination_decision', 'source_administration', 'destination_administration', 'source_payment', 'destination_payment'): main_value = main_delivery.getProperty(attr) value = delivery.getProperty(attr) if main_value != value: raise self.MergeDeliveryListError, \ "%s is not the same between %s and %s (%s and %s)" % (attr, delivery.getId(), main_delivery.getId(), value, main_value) # One more sanity check. Check if discounts are the same, if any. main_discount_list = main_delivery.contentValues(filter = {'portal_type': self.getPortalDiscountTypeList()}) for delivery in delivery_list: discount_list = delivery.contentValues(filter = {'portal_type': self.getPortalDiscountTypeList()}) if len(main_discount_list) != len(discount_list): raise self.MergeDeliveryListError, "Discount is not the same between %s and %s" % (delivery.getId(), main_delivery.getId()) for discount in discount_list: for main_discount in main_discount_list: if discount.getDiscount() == main_discount.getDiscount() \ and discount.getDiscountRatio() == main_discount.getDiscountRatio() \ and discount.getDiscountType() == main_discount.getDiscountType() \ and discount.getImmediateDiscount() == main_discount.getImmediateDiscount(): break else: raise self.MergeDeliveryListError, "Discount is not the same between %s and %s" % (delivery.getId(), main_delivery.getId()) # One more sanity check. Check if payment conditions are the same, if any. main_payment_condition_list = main_delivery.contentValues(filter = {'portal_type': self.getPortalPaymentConditionTypeList()}) for delivery in delivery_list: payment_condition_list = delivery.contentValues(filter = {'portal_type': self.getPortalPaymentConditionTypeList()}) if len(main_payment_condition_list) != len(payment_condition_list): raise self.MergeDeliveryListError, "Payment Condition is not the same between %s and %s" % (delivery.getId(), main_delivery.getId()) for condition in payment_condition_list: for main_condition in main_payment_condition_list: if condition.getPaymentMode() == main_condition.getPaymentMode() \ and condition.getPaymentAdditionalTerm() == main_condition.getPaymentAdditionalTerm() \ and condition.getPaymentAmount() == main_condition.getPaymentAmount() \ and condition.getPaymentEndOfMonth() == main_condition.getPaymentEndOfMonth() \ and condition.getPaymentRatio() == main_condition.getPaymentRatio() \ and condition.getPaymentTerm() == main_condition.getPaymentTerm(): break else: raise self.MergeDeliveryListError, "Payment Condition is not the same between %s and %s" % (delivery.getId(), main_delivery.getId()) # Make sure that all activities are flushed, to get simulation movements from delivery cells. for delivery in delivery_list: for order in delivery.getCausalityValueList(portal_type = self.getPortalOrderTypeList()): for applied_rule in order.getCausalityRelatedValueList(portal_type = 'Applied Rule'): applied_rule.flushActivity(invoke = 1) for causality_related_delivery in delivery.getCausalityValueList(portal_type = self.getPortalDeliveryTypeList()): for applied_rule in causality_related_delivery.getCausalityRelatedValueList(portal_type = 'Applied Rule'): applied_rule.flushActivity(invoke = 1) # Get a list of simulated movements and invoice movements. main_simulated_movement_list = main_delivery.getSimulatedMovementList() main_invoice_movement_list = main_delivery.getInvoiceMovementList() simulated_movement_list = main_simulated_movement_list[:] invoice_movement_list = main_invoice_movement_list[:] for delivery in delivery_list: simulated_movement_list.extend(delivery.getSimulatedMovementList()) invoice_movement_list.extend(delivery.getInvoiceMovementList()) #for movement in simulated_movement_list + invoice_movement_list: # parent = movement.aq_parent # LOG('mergeDeliveryList', 0, 'movement = %s, parent = %s, movement.getPortalType() = %s, parent.getPortalType() = %s' % (repr(movement), repr(parent), repr(movement.getPortalType()), repr(parent.getPortalType()))) LOG('mergeDeliveryList', 0, 'simulated_movement_list = %s, invoice_movement_list = %s' % (str(simulated_movement_list), str(invoice_movement_list))) for main_movement_list, movement_list in \ ((main_simulated_movement_list, simulated_movement_list), (main_invoice_movement_list, invoice_movement_list)): root_group = self.collectMovement(movement_list, check_order = 0, check_path = 0, check_date = 0, check_criterion = 1, check_resource = 1, check_base_variant = 1, check_variant = 1) for criterion_group in root_group.group_list: LOG('mergeDeliveryList dump tree', 0, 'criterion = %s, movement_list = %s, group_list = %s' % (repr(criterion_group.criterion), repr(criterion_group.movement_list), repr(criterion_group.group_list))) for resource_group in criterion_group.group_list: LOG('mergeDeliveryList dump tree', 0, 'resource = %s, movement_list = %s, group_list = %s' % (repr(resource_group.resource), repr(resource_group.movement_list), repr(resource_group.group_list))) for base_variant_group in resource_group.group_list: LOG('mergeDeliveryList dump tree', 0, 'base_category_list = %s, movement_list = %s, group_list = %s' % (repr(base_variant_group.base_category_list), repr(base_variant_group.movement_list), repr(base_variant_group.group_list))) for variant_group in base_variant_group.group_list: LOG('mergeDeliveryList dump tree', 0, 'category_list = %s, movement_list = %s, group_list = %s' % (repr(variant_group.category_list), repr(variant_group.movement_list), repr(variant_group.group_list))) for criterion_group in root_group.group_list: for resource_group in criterion_group.group_list: for base_variant_group in resource_group.group_list: # Get a list of categories. category_dict = {} for variant_group in base_variant_group.group_list: for category in variant_group.category_list: category_dict[category] = 1 category_list = category_dict.keys() # Try to find a delivery line. delivery_line = None for movement in base_variant_group.movement_list: if movement in main_movement_list: if movement.aq_parent.getPortalType() in self.getPortalSimulatedMovementTypeList() \ or movement.aq_parent.getPortalType() in self.getPortalInvoiceMovementTypeList(): delivery_line = movement.aq_parent else: delivery_line = movement LOG('mergeDeliveryList', 0, 'delivery_line %s is found: criterion = %s, resource = %s, base_category_list = %s' % (repr(delivery_line), repr(criterion_group.criterion), repr(resource_group.resource), repr(base_variant_group.base_category_list))) break if delivery_line is None: # Not found. So create a new delivery line. movement = base_variant_group.movement_list[0] if movement.aq_parent.getPortalType() in self.getPortalSimulatedMovementTypeList() \ or movement.aq_parent.getPortalType() in self.getPortalInvoiceMovementTypeList(): delivery_line_type = movement.aq_parent.getPortalType() else: delivery_line_type = movement.getPortalType() delivery_line = main_delivery.newContent(portal_type = delivery_line_type, resource = resource_group.resource) LOG('mergeDeliveryList', 0, 'New delivery_line %s is created: criterion = %s, resource = %s, base_category_list = %s' % (repr(delivery_line), repr(criterion_group.criterion), repr(resource_group.resource), repr(base_variant_group.base_category_list))) # Update the base categories and categories. #LOG('mergeDeliveryList', 0, 'base_category_list = %s, category_list = %s' % (repr(base_category_list), repr(category_list))) delivery_line.setVariationBaseCategoryList(base_variant_group.base_category_list) delivery_line.setVariationCategoryList(category_list) object_to_update = None for variant_group in base_variant_group.group_list: if len(variant_group.category_list) == 0: object_to_update = delivery_line else: for delivery_cell in delivery_line.contentValues(): predicate_value_list = delivery_cell.getPredicateValueList() LOG('mergeDeliveryList', 0, 'delivery_cell = %s, predicate_value_list = %s, variant_group.category_list = %s' % (repr(delivery_cell), repr(predicate_value_list), repr(variant_group.category_list))) if len(predicate_value_list) == len(variant_group.category_list): for category in variant_group.category_list: if category not in predicate_value_list: break else: object_to_update = delivery_cell break #LOG('mergeDeliveryList', 0, 'object_to_update = %s' % repr(object_to_update)) if object_to_update is not None: cell_price = object_to_update.getPrice() or 0.0 cell_quantity = object_to_update.getQuantity() or 0.0 cell_target_quantity = object_to_update.getNetConvertedTargetQuantity() or 0.0 # XXX What to do ? cell_total_price = cell_target_quantity * cell_price cell_category_list = list(object_to_update.getCategoryList()) for movement in variant_group.movement_list: if movement in main_movement_list: continue LOG('mergeDeliveryList', 0, 'movement = %s' % repr(movement)) cell_quantity += movement.getQuantity() cell_target_quantity += movement.getNetConvertedTargetQuantity() try: # XXX WARNING - ADD PRICED QUANTITY cell_price = movement.getPrice() cell_total_price += movement.getNetConvertedTargetQuantity() * cell_price except: cell_total_price = None for category in movement.getCategoryList(): if category not in cell_category_list: cell_category_list.append(category) # Make sure that simulation movements point to an appropriate delivery line or # delivery cell. if hasattr(movement, 'getDeliveryRelatedValueList'): for simulation_movement in \ movement.getDeliveryRelatedValueList(portal_type = 'Simulation Movement'): simulation_movement.setDeliveryValue(object_to_update) #simulation_movement.reindexObject() if hasattr(movement, 'getOrderRelatedValueList'): for simulation_movement in \ movement.getOrderRelatedValueList(portal_type = 'Simulation Movement'): simulation_movement.setOrderValue(object_to_update) #simulation_movement.reindexObject() if cell_target_quantity != 0 and cell_total_price is not None: average_price = cell_total_price / cell_target_quantity else: average_price = 0 LOG('mergeDeliveryList', 0, 'object_to_update = %s, cell_category_list = %s, cell_target_quantity = %s, cell_quantity = %s, average_price = %s' % (repr(object_to_update), repr(cell_category_list), repr(cell_target_quantity), repr(cell_quantity), repr(average_price))) object_to_update.setCategoryList(cell_category_list) if object_to_update.getPortalType() in self.getPortalSimulatedMovementTypeList(): object_to_update.edit(target_quantity = cell_target_quantity, quantity = cell_quantity, price = average_price, ) elif object_to_update.getPortalType() in self.getPortalInvoiceMovementTypeList(): # Invoices do not have target quantities, and the price never change. object_to_update.edit(quantity = cell_quantity, price = cell_price, ) else: raise self.MergeDeliveryListError, "Unknown portal type %s" % str(object_to_update.getPortalType()) #object_to_update.immediateReindexObject() else: raise self.MergeDeliveryListError, "No object to update" # Merge containers. Just copy them from other deliveries into the main. for delivery in delivery_list: container_id_list = delivery.contentIds(filter = {'portal_type': self.getPortalContainerTypeList()}) if len(container_id_list) > 0: copy_data = delivery.manage_copyObjects(ids = container_id_list) new_id_list = main_delivery.manage_pasteObjects(copy_data) # Unify the list of causality. causality_list = main_delivery.getCausalityValueList() for delivery in delivery_list: for causality in delivery.getCausalityValueList(): if causality not in causality_list: causality_list.append(causality) LOG("mergeDeliveryList", 0, "causality_list = %s" % str(causality_list)) main_delivery.setCausalityValueList(causality_list) # Cancel deliveries. for delivery in delivery_list: LOG("mergeDeliveryList", 0, "cancelling %s" % repr(delivery)) delivery.cancel() # Reindex the main delivery. main_delivery.reindexObject() return main_delivery InitializeClass(SimulationTool)