From ffe2fa9f514847b80aa4468cb53274e1d2030cae Mon Sep 17 00:00:00 2001
From: Guillaume Michon <guillaume@nexedi.com>
Date: Mon, 18 Apr 2005 06:01:44 +0000
Subject: [PATCH] Modified divergence detection

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@2882 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/Delivery.py           | 325 +++++++-------------
 product/ERP5/Document/SimulationMovement.py |  71 ++++-
 2 files changed, 183 insertions(+), 213 deletions(-)

diff --git a/product/ERP5/Document/Delivery.py b/product/ERP5/Document/Delivery.py
index 6e50db3e5f..eabf9ce145 100755
--- a/product/ERP5/Document/Delivery.py
+++ b/product/ERP5/Document/Delivery.py
@@ -40,6 +40,10 @@ from Products.PythonScripts.Utility import allow_class
 from DateTime import DateTime
 #from Products.ERP5.ERP5Globals import movement_type_list, draft_order_state, planned_order_state
 
+from Products.ERP5.MovementGroup import OrderMovementGroup, PathMovementGroup
+from Products.ERP5.MovementGroup import DateMovementGroup, ResourceMovementGroup
+from Products.ERP5.MovementGroup import VariantMovementGroup, RootMovementGroup
+
 from zLOG import LOG
 
 class TempDeliveryCell(DeliveryCell):
@@ -518,7 +522,7 @@ class Delivery(XMLObject):
     def getTotalPrice(self):
       """
       """
-      result = self.z_total_price(delivery_uid = self.getUid())
+      result = self.z_total_price(explanation_uid = self.getUid())
       return result[0][0]
 
 #     security.declareProtected(Permissions.AccessContentsInformation, 'getTotalPrice')
@@ -557,7 +561,7 @@ class Delivery(XMLObject):
       """
         Returns the total price for this order
       """
-      kw['delivery_uid'] = self.getUid()
+      kw['explanation_uid'] = self.getUid()
       kw.update(self.portal_catalog.buildSQLQuery(**kw))
       if src__:
         return self.Delivery_zGetTotal(src__=1, **kw)
@@ -569,7 +573,7 @@ class Delivery(XMLObject):
       """
         Returns the quantity if no cell or the total quantity if cells
       """      
-      kw['delivery_uid'] = self.getUid()
+      kw['explanation_uid'] = self.getUid()
       kw.update(self.portal_catalog.buildSQLQuery(**kw))
       if src__:
         return self.Delivery_zGetTotal(src__=1, **kw)
@@ -687,150 +691,6 @@ class Delivery(XMLObject):
           # else Do we need to create a simulation movement ? XXX probably not
       return 1
 
-    security.declareProtected(Permissions.View, 'isArrowDivergent')
-    def isArrowDivergent(self):
-      LOG('Delivery.isArrowDivergent, self.isSourceDivergent()',0,self.isSourceDivergent())
-      LOG('Delivery.isArrowDivergent, self.isDestinationDivergent()',0,self.isDestinationDivergent())
-      LOG('Delivery.isArrowDivergent, self.isSourceSectionDivergent()',0,self.isSourceSectionDivergent())
-      LOG('Delivery.isArrowDivergent, self.isDestinationSectionDivergent()',0,self.isDestinationSectionDivergent())
-      if self.isSourceDivergent(): return 1
-      if self.isDestinationDivergent(): return 1
-      if self.isSourceSectionDivergent(): return 1
-      if self.isDestinationSectionDivergent(): return 1
-      return 0
-
-    security.declareProtected(Permissions.View, 'isSourceDivergent')
-    def isSourceDivergent(self):
-      """
-        Source is divergent if simulated and target values differ
-        or if multiple sources are defined
-      """
-      if self.getSource() != self.getSimulationSource() \
-          or len(self.getSourceList()) > 1 \
-          or len(self.getSimulationSourceList()) > 1:
-        return 1
-      return 0
-
-    security.declareProtected(Permissions.View, 'isDestinationDivergent')
-    def isDestinationDivergent(self):
-      """
-        Destination is divergent if simulated and target values differ
-        or if multiple destinations are defined
-      """
-      LOG('Delivery.isDestinationDivergent, self.getPath()',0,self.getPath())
-      LOG('Delivery.isDestinationDivergent, self.getDestination()',0,self.getDestination())
-      LOG('Delivery.isDestinationDivergent, self.getSimulationDestination()',0,self.getSimulationDestination())
-      LOG('Delivery.isDestinationDivergent, self.getDestinationList()',0,self.getDestinationList())
-      LOG('Delivery.isDestinationDivergent, self.getSimulationDestinationList()',0,self.getSimulationDestinationList())
-      if self.getDestination() != self.getSimulationDestination() \
-          or len(self.getDestinationList()) > 1 \
-          or len(self.getSimulationDestinationList()) > 1:
-        return 1
-      return 0
-
-    security.declareProtected(Permissions.View, 'isSourceSectionDivergent')
-    def isSourceSectionDivergent(self):
-      """
-        Same as isSourceDivergent for source_section
-      """
-      if self.getSourceSection() != self.getSimulationSourceSection() \
-          or len(self.getSourceSectionList()) > 1 \
-          or len(self.getSimulationSourceSectionList()) > 1:
-        return 1
-      return 0
-
-    security.declareProtected(Permissions.View, 'isDestinationSectionDivergent')
-    def isDestinationSectionDivergent(self):
-      """
-        Same as isDestinationDivergent for source_section
-      """
-      if self.getDestinationSection() != self.getSimulationDestinationSection() \
-          or len(self.getDestinationSectionList()) > 1 \
-          or len(self.getSimulationDestinationSectionList()) > 1:
-        return 1
-      return 0
-
-    security.declareProtected(Permissions.View, 'isDateDivergent')
-    def isDateDivergent(self):
-      """
-      """
-      LOG("isDivergent getStartDate", 0, repr(self.getStartDate()))
-      LOG("isDivergent getSimulationStartDate", 0, repr(self.getSimulationStartDate()))
-      LOG("isDivergent getStopDate", 0, repr(self.getStopDate()))
-      LOG("isDivergent getSimulationStopDate", 0, repr(self.getSimulationStopDate()))
-      from DateTime import DateTime
-      if self.getStartDate() is None or self.getSimulationStartDate() is None \
-               or self.getStopDate() is None or self.getSimulationStopDate() is None:
-        return 1
-      # This is uggly but required due to python2.2/2.3 Zope 2.6/2.7 inconsistency in _millis calculation
-      if self.getStartDate().Date() != self.getSimulationStartDate().Date()  or \
-         self.getStopDate().Date() != self.getSimulationStopDate().Date():
-#         LOG("isDivergent getStartDate", 0, repr(self.getStartDate()))
-#         LOG("isDivergent getTargetStartDate", 0, repr(self.getTargetStartDate()))
-#         LOG("isDivergent getStopDate", 0, repr(self.getStopDate()))
-#         LOG("isDivergent getTargetStopDate", 0, repr(self.getTargetStopDate()))
-#
-#         LOG("isDivergent getStartDate", 0, repr(self.getStartDate()))
-#         LOG("isDivergent getTargetStartDate", 0, repr(self.getTargetStartDate()))
-#         LOG("isDivergent getStopDate", 0, repr(self.getStopDate()))
-#         LOG("isDivergent getTargetStopDate", 0, repr(self.getTargetStopDate()))
-#         LOG("isDivergent getStartDate", 0, repr(self.getStartDate()._millis))
-#         LOG("isDivergent getTargetStartDate", 0, repr(self.getTargetStartDate()._millis))
-#         LOG("isDivergent getStopDate", 0, repr(self.getStopDate()._millis))
-#         LOG("isDivergent getTargetStopDate", 0, repr(self.getTargetStopDate()._millis))
-#         LOG("isDivergent class getStartDate", 0, repr(self.getStartDate().__class__))
-#         LOG("isDivergent class getTargetStartDate", 0, repr(self.getTargetStartDate().__class__))
-#         LOG("isDivergent class getStopDate", 0, repr(self.getStopDate().__class__))
-#         LOG("isDivergent class getTargetStopDate", 0, repr(self.getTargetStopDate().__class__))
-#         LOG("isDivergent", 0, repr(type(self.getStartDate())))
-#         LOG("isDivergent", 0, repr(type(self.getTargetStartDate())))
-#         LOG("isDivergent ==", 0, str(self.getStartDate() == self.getTargetStartDate()))
-#         LOG("isDivergent !=", 0, str(self.getStartDate() != self.getTargetStartDate()))
-#         LOG("isDivergent", 0, str(self.getStopDate() != self.getTargetStopDate()))
-        return 1
-
-    security.declareProtected(Permissions.View, 'isQuantityDivergent')
-    def isQuantityDivergent(self):
-      """
-      """
-      for line in self.contentValues(filter={'portal_type': self.getPortalMovementTypeList()}):
-        if line.isDivergent():
-          return 1
-
-    security.declareProtected(Permissions.View, 'isQuantityDivergent')
-    def isResourceDivergent(self):
-      """
-      We look at all lines if we have changed the resource, and by
-      doing so we are not consistent with the simulation
-      """
-      LOG('Delivery.isResourceDivergent, self.getPath()',0,self.getPath())
-      if self.isSimulated():
-        LOG('Delivery.isResourceDivergent, self.isSimulated()',0,self.isSimulated())
-        for l in self.contentValues(filter={'portal_type':self.getPortalDeliveryMovementTypeList()}):
-          LOG('Delivery.isResourceDivergent, l.getPath()',0,l.getPath())
-          resource = l.getResource()
-          LOG('Delivery.isResourceDivergent, line_resource',0,l.getResource())
-          simulation_resource_list = l.getDeliveryRelatedValueList()
-          simulation_resource_list += l.getOrderRelatedValueList()
-          for simulation_resource in simulation_resource_list:
-            LOG('Delivery.isResourceDivergent, sim_resource',0,simulation_resource.getResource())
-            if simulation_resource.getResource()!= resource:
-              return 1
-
-        for m in self.getMovementList():
-          LOG('Delivery.isResourceDivergent, m.getPath()',0,m.getPath())
-          resource = m.getResource()
-          LOG('Delivery.isResourceDivergent, resource',0,resource)
-          simulation_resource_list = m.getDeliveryRelatedValueList()
-          for simulation_resource in simulation_resource_list:
-            LOG('Delivery.isResourceDivergent, sim_resource',0,simulation_resource.getResource())
-            if simulation_resource.getResource()!= resource:
-              return 1
-            #delivery_cell_related_list = c.getDeliveryRelatedValueList()
-          pass
-
-      return 0
-
     security.declareProtected(Permissions.View, 'isDivergent')
     def isDivergent(self):
       """
@@ -840,15 +700,16 @@ class Delivery(XMLObject):
 
         emit targetUnreachable !
       """
-      LOG('Delivery.isDivergent, self.isArrowDivergent()',0,self.isArrowDivergent())
-      LOG('Delivery.isDivergent, self.isDateDivergent()',0,self.isDateDivergent())
-      LOG('Delivery.isDivergent, self.isQuantityDivergent()',0,self.isQuantityDivergent())
-      LOG('Delivery.isDivergent, self.isResourceDivergent()',0,self.isResourceDivergent())
-      if self.isArrowDivergent(): return 1
-      if self.isDateDivergent(): return 1
-      if self.isQuantityDivergent(): return 1
-      if self.isResourceDivergent(): return 1
-
+      if len(self.Delivery_zIsDivergent(uid=self.getUid())) > 0:
+        return 1
+      # Check if the total quantity equals the total of each simulation movement quantity
+      for movement in self.getMovementList():
+        d_quantity = movement.getQuantity()
+        simulation_quantity = 0.
+        for simulation_movement in movement.getDeliveryRelatedValueList():
+          simulation_quantity += float(simulation_movement.getCorrectedQuantity())
+        if d_quantity != simulation_quantity:
+          return 1
       return 0
 
     security.declareProtected(Permissions.ModifyPortalContent, 'solve')
@@ -1110,64 +971,72 @@ class Delivery(XMLObject):
     security.declareProtected(Permissions.ModifyPortalContent, 'updateFromSimulation')
     def updateFromSimulation(self, update_target = 0):
       """
-        Updates all lines and cells of this delivery based on movements
-        in the simulation related to this delivery through the delivery relation
-
-        Error: resource in sim could change - we should disconnect in this case
-      """
-      source_list = []
-      destination_list = []
-      target_source_list = []
-      target_destination_list = []
+      Update all lines of this transaction based on movements in the simulation
+      related to this transaction.
+      """
+      # XXX update_target no more used
+      transaction_type = self.getPortalType()
+      line_type = transaction_type + " Line"
+      to_aggregate_movement_list = []
+      to_reindex_list = []
+      source_section = self.getSourceSection()
+      destination_section = self.getDestinationSection()
+      resource = self.getResource()
+      start_date = self.getStartDate()
+      
+      def updateLineOrCell(c):
+        quantity = 0
+        source = c.getSource()
+        destination = c.getDestination()
+        for m in c.getDeliveryRelatedValueList():
+          m_source_section = m.getSourceSection()
+          m_destination_section = m.getDestinationSection()
+          m_resource = m.getResource()
+          m_start_date = m.getStartDate()
+          m_source = m.getSource()
+          m_destination = m.getDestination()
+          m_quantity = m.getCorrectedQuantity()
+          if m_source_section == source_section and m_destination_section == destination_section \
+              and m_resource == resource and m_start_date == start_date:
+            if m_source == source and m_destination == destination:
+              # The path is the same, only the quantity may have changed
+              if m_quantity:
+                quantity += m_quantity
+            else:
+              # Source and/or destination have changed. The Simulation Movement has
+              # to be linked to a new TransactionLine
+              m.setDelivery('')
+              to_aggregate_movement_list.append(m)
+            to_reindex_list.append(m)
+          else:
+            # Source_section and/or destination_section and/or date and/or resource differ
+            # The Simulation Movement has to be linked to a new Transaction (or an existing one)
+            m.setDelivery('')
+            to_aggregate_movement_list.append(m)
+            to_reindex_list.append(m)
+        # Recalculate delivery ratios for the remaining movements in this line
+        c.setQuantity(quantity)
+        c.updateSimulationDeliveryProperties()
+      
+      # Update the transaction from simulation
       for l in self.contentValues(filter={'portal_type':self.getPortalDeliveryMovementTypeList()}):
         if l.hasCellContent():
           for c in l.contentValues(filter={'portal_type':self.getPortalDeliveryMovementTypeList()}):
-            #source_list.extend(c.getSimulationSourceList())
-            delivery_cell_related_list = c.getDeliveryRelatedValueList()
-            delivery_cell_related_list = [x for x in delivery_cell_related_list if (x.getId()!='produced_resource')]
-            source_list.extend(map(lambda x: x.getSource(),delivery_cell_related_list))
-            LOG('Delivery.updateFromSimulation, source_list:',0,source_list)
-            LOG('Delivery.updateFromSimulation, delivery_cell_related_list:',0,[x.getPhysicalPath() for x in delivery_cell_related_list])
-            target_source_list.extend(map(lambda x: x.getTargetSource(),delivery_cell_related_list))
-            #destination_list.extend(c.getDestinationSourceList())
-            destination_list.extend(map(lambda x: x.getDestination(),delivery_cell_related_list))
-            target_destination_list.extend(map(lambda x: x.getTargetDestination(),delivery_cell_related_list))
-            simulation_quantity = sum(map(lambda x: x.getQuantity(),delivery_cell_related_list))
-            #c._setQuantity(c.getSimulationQuantity()) # Only update quantity here
-            c._setQuantity(simulation_quantity) # Only update quantity here
-            if update_target:
-              simulation_target_quantity = sum(map(lambda x: x.getTargetQuantity(),delivery_cell_related_list))
-              c._setTargetQuantity(simulation_target_quantity)
+            updateLineOrCell(c)
         else:
-          delivery_line_related_list = l.getDeliveryRelatedValueList()
-          #source_list.extend(l.getSimulationSourceList())
-          source_list.extend(map(lambda x: x.getSource(),delivery_line_related_list))
-          target_source_list.extend(map(lambda x: x.getTargetSource(),delivery_line_related_list))
-          destination_list.extend(map(lambda x: x.getDestination(),delivery_line_related_list))
-          target_destination_list.extend(map(lambda x: x.getTargetDestination(),delivery_line_related_list))
-          simulation_quantity = sum(map(lambda x: x.getQuantity(),delivery_line_related_list))
-          l._setQuantity(simulation_quantity) # Only update quantity here
-          if update_target:
-            simulation_target_quantity = sum(map(lambda x: x.getTargetQuantity(),delivery_line_related_list))
-            c._setTargetQuantity(simulation_target_quantity)
-      # Update source list
-      LOG('Delivery.updateFromSimulation, source_list:',0,source_list)
-      LOG('Delivery.updateFromSimulation, destination_list:',0,destination_list)
-      LOG('Delivery.updateFromSimulation, target_source_list:',0,target_source_list)
-      LOG('Delivery.updateFromSimulation, target_destination_list:',0,target_destination_list)
-      if not None in source_list:
-        self._setSourceSet(source_list) # Set should make sure each item is only once
-      if not None in destination_list:
-        self._setDestinationSet(destination_list) 
-      if update_target:
-        if not None in target_source_list:
-          LOG('Delivery.updateFromSimulation, update_target_source:',0,target_source_list)
-          self._setTargetSourceSet(target_source_list) # Set should make sure each item is only once
-        if not None in target_destination_list:
-          LOG('Delivery.updateFromSimulation, update_target_destination:',0,target_destination_list)
-          self._setTargetDestinationSet(target_destination_list) 
-      self.edit() # so that we may go to converged state
-      
+          updateLineOrCell(l)
+          
+      # Re-aggregate the disconnected movements
+      # XXX Dirty ; it should use DeliveryBuilder when it will be available
+      if len(to_aggregate_movement_list) > 0:
+        applied_rule_type = to_aggregate_movement_list[0].getRootAppliedRule().getSpecialiseId()
+        if applied_rule_type == "default_amortisation_rule":
+          self.portal_simulation.buildDeliveryList( self.portal_simulation.collectMovement(to_aggregate_movement_list,
+                                                        [ResourceMovementGroup, DateMovementGroup, PathMovementGroup] ) )
+        
+      # Touch the Transaction to make an automatic converge
+      self.edit() 
+
     security.declareProtected(Permissions.ModifyPortalContent, 'propagateResourceToSimulation')
     def propagateResourceToSimulation(self):
       """
@@ -1178,6 +1047,8 @@ class Delivery(XMLObject):
 
         propagateResourceToSimulation has priority (ie. must be executed before) over updateFromSimulation
       """
+      if self.getPortalType() == 'Amortisation Transaction':
+        return
       unmatched_simulation_movement = []
       unmatched_delivery_movement = []
       LOG('propagateResourceToSimulation, ',0,'starting')
@@ -1274,3 +1145,37 @@ class Delivery(XMLObject):
       #if self.getSimulationState() in planned_order_state:
       #  self.updateAppliedRule() # This should be implemented with the interaction tool rather than with this hard coding
 
+    security.declareProtected(Permissions.ModifyPortalContent, 'notifySimulationChange')
+    def notifySimulationChange(self):
+      """
+        WorkflowMethod used to notify the causality workflow that the simulation
+        has changed, so we have to check if the delivery is divergent or not
+      """
+      pass
+    notifySimulationChange = WorkflowMethod(notifySimulationChange)
+
+    def updateSimulationDeliveryProperties(self, movement_list = None, delivery = None):
+      """
+      Set properties delivery_ratio and delivery_error for each simulation movement
+      in movement_list (all movements by default), according to this delivery calculated quantity
+      """
+      if movement_list is None:
+        movement_list = delivery.getDeliveryRelatedValueList()
+      # First find the calculated quantity
+      delivery_quantity = 0
+      for m in delivery.getDeliveryRelatedValueList():
+        m_quantity = m.getCorrectedQuantity()
+        if m_quantity is not None:
+          delivery_quantity += m_quantity
+      # Then set the properties
+      if delivery_quantity != 0:
+        for m in movement_list:
+          m.setDeliveryRatio(m.getCorrectedQuantity() / delivery_quantity)
+          m.setDeliveryError(delivery_quantity * m.getDeliveryRatio() - m.getCorrectedQuantity())
+      else:
+        for m in movement_list:
+          m.setDeliveryError(m.getCorrectedQuantity())
+          m.setProfitQuantity(m.getQuantity())
+      # Finally, reindex the movements to update their divergence property
+      for m in delivery.getDeliveryRelatedValueList():
+        m.immediateReindexObject()
diff --git a/product/ERP5/Document/SimulationMovement.py b/product/ERP5/Document/SimulationMovement.py
index bfc83e7046..9cf74e1a69 100755
--- a/product/ERP5/Document/SimulationMovement.py
+++ b/product/ERP5/Document/SimulationMovement.py
@@ -313,7 +313,7 @@ a service in a public administration)."""
                  'simulation_state'               : self.getSimulationState(),
                  'order_uid'                      : self.getOrderUid(),
                  'explanation_uid'                : self.getExplanationUid(),
-                 'delivery_uid'                   : self.getDeliveryUid(),
+                 #'delivery_uid'                   : self.getDeliveryUid(),
                  'source_uid'                     : self.getSourceUid(),
                  'destination_uid'                : self.getDestinationUid(),
                  'source_section_uid'             : self.getSourceSectionUid(),
@@ -387,7 +387,12 @@ a service in a public administration)."""
         # Ex. zero stock rule
         return ra.getUid()
     else:
-      return self.getDeliveryUid()
+      explanation_value = self.getDeliveryValue()
+      while explanation_value.getPortalType() not in self.getPortalDeliveryTypeList() and \
+          explanation_value != self.getPortalObject():
+            explanation_value = explanation_value.getParent()
+      if explanation_value != self.getPortalObject():
+        return explanation_value.getUid()
 
   security.declareProtected(Permissions.AccessContentsInformation, 'getExplanationValue')
   def getExplanationValue(self):
@@ -449,4 +454,64 @@ a service in a public administration)."""
     if order_value is not None:
       return order_value.getStopDate()
   
-  
\ No newline at end of file
+  security.declareProtected(Permissions.AccessContentsInformation, 'isConvergent')
+  def isConvergent(self):
+    """
+      Returns true if the Simulation Movement is convergent comparing to the delivery value
+    """
+    return not self.isDivergent()
+
+  security.declareProtected(Permissions.AccessContentsInformation, 'isDivergent')
+  def isDivergent(self):
+    """
+      Returns true if the Simulation Movement is divergent comparing to the delivery value
+    """
+    delivery = self.getDeliveryValue()
+    if delivery is None:
+      return 0
+    if self.getSourceSection()      != delivery.getSourceSection() or \
+       self.getDestinationSection() != delivery.getDestinationSection() or \
+       self.getSource()             != delivery.getSource() or \
+       self.getDestination()        != delivery.getDestination() or \
+       self.getResource()           != delivery.getResource() or \
+       self.getStartDate()          != delivery.getStartDate() or \
+       self.getStopDate()           != delivery.getStopDate():
+      return 1
+
+    d_quantity = delivery.getQuantity()
+    quantity = self.getCorrectedQuantity()
+    d_error = self.getDeliveryError()
+    if quantity is None:
+      if d_quantity is None:
+        return 0
+      return 1
+    if d_error is None:
+      d_error = 0
+    delivery_ratio = self.getDeliveryRatio()
+    if delivery_ratio is not None:
+      d_quantity *= delivery_ratio 
+    if d_quantity != quantity + d_error:
+      return 1
+    return 0  
+ 
+  security.declareProtected(Permissions.View, 'setDefaultDeliveryProperties')
+  def setDefaultDeliveryProperties(self):
+    """
+    Sets the delivery_ratio and delivery_error properties to the calculated value
+    """
+    delivery = self.getDeliveryValue()
+    if delivery is not None:
+      delivery.updateSimulationDeliveryProperties(movement_list = [self])
+
+  security.declareProtected(Permissions.View, 'getCorrectedQuantity')
+  def getCorrectedQuantity(self):
+    """
+    Returns the quantity property deducted by the possible profit_quantity
+    """
+    quantity = self.getQuantity()
+    profit_quantity = self.getProfitQuantity()
+    if quantity is not None:
+      if profit_quantity:
+        return quantity - profit_quantity
+      return quantity
+    return None
-- 
2.30.9