From 438566357a9d4eecc2d9df88341e57f288f4b845 Mon Sep 17 00:00:00 2001
From: Sebastien Robin <seb@nexedi.com>
Date: Fri, 7 Mar 2014 17:43:14 +0100
Subject: [PATCH] Order Builders: remove the whole testOrderBuilder passing

So all newSimulation expected failures are removed. Make
generateMovementListForStockOptimisation looking min_flow and max_delay
on supply lines.

Introduce a getNextAlertInventoryDate in addition to
getNextNegativeInventoryDate. This allows to know when an inventory
will be below a reference quantity. This is particularly helpful
to know when an inventory is below the minimal admitted stock
---
 product/ERP5/Document/Resource.py      | 16 +++++--
 product/ERP5/Tool/SimulationTool.py    | 50 ++++++++++++--------
 product/ERP5/mixin/builder.py          | 38 ++++++++++-----
 product/ERP5/tests/testOrderBuilder.py | 64 +++++++++++++-------------
 4 files changed, 103 insertions(+), 65 deletions(-)

diff --git a/product/ERP5/Document/Resource.py b/product/ERP5/Document/Resource.py
index 61e4ff6783..0996a62c71 100644
--- a/product/ERP5/Document/Resource.py
+++ b/product/ERP5/Document/Resource.py
@@ -565,12 +565,22 @@ class Resource(XMLObject, XMLMatrix, VariatedMixin):
                               'getNextNegativeInventoryDate')
     def getNextNegativeInventoryDate(self, **kw):
       """
-      Returns list of inventory grouped by section or site
+      Returns next date where the inventory will be negative
+      """
+      return self.getNextAlertInventoryDate(
+                  reference_quantity=0, **kw)
+
+    security.declareProtected(Permissions.AccessContentsInformation,
+                              'getNextNegativeInventoryDate')
+    def getNextAlertInventoryDate(self, reference_quantity=0, **kw):
+      """
+      Returns next date where the inventory will be below reference
+      quantity
       """
       kw['resource_uid'] = self.getUid()
       portal_simulation = self.getPortalObject().portal_simulation
-      return portal_simulation.getNextNegativeInventoryDate(**kw)
-
+      return portal_simulation.getNextAlertInventoryDate(
+                          reference_quantity=reference_quantity, **kw)
 
     # Asset Price API
     security.declareProtected(Permissions.AccessContentsInformation,
diff --git a/product/ERP5/Tool/SimulationTool.py b/product/ERP5/Tool/SimulationTool.py
index a3d1b4340f..e96f82939b 100644
--- a/product/ERP5/Tool/SimulationTool.py
+++ b/product/ERP5/Tool/SimulationTool.py
@@ -2048,29 +2048,41 @@ class SimulationTool(BaseTool):
       assert len(inventory_list) == 1
       return inventory_list[0]
 
+    security.declareProtected(Permissions.AccessContentsInformation,
+                              'getNextDeficientInventoryDate')
+    def getNextAlertInventoryDate(self, reference_quantity=0, src__=0, **kw):
+      """
+      Give the next date where the quantity is lower than the
+      reference quantity.
+      """
+      result = None
+      # First look at current inventory, we might have already an inventory
+      # lower than reference_quantity
+      current_inventory = self.getCurrentInventory(**kw)
+      if current_inventory < reference_quantity:
+        result = DateTime()
+      else:
+        result = self.getInventoryList(src__=src__,
+            sort_on = (('date', 'ascending'),), group_by_movement=1, **kw)
+        if src__ :
+          return result
+        total_inventory = 0.
+        for inventory in result:
+          if inventory['inventory'] is not None:
+            total_inventory += inventory['inventory']
+            if total_inventory < reference_quantity:
+              result = inventory['date']
+              break
+      return result
+
     security.declareProtected(Permissions.AccessContentsInformation,
                               'getNextNegativeInventoryDate')
-    def getNextNegativeInventoryDate(self, src__=0, **kw):
+    def getNextNegativeInventoryDate(self, **kw):
       """
-      Returns statistics of inventory grouped by section or site
+      Deficient Inventory with a reference_quantity of 0, so when the
+      stock will be negative
       """
-      #sql_kw = self._generateSQLKeywordDict(order_by_expression='stock.date', **kw)
-      #sql_kw['group_by_expression'] = 'stock.uid'
-      #sql_kw['order_by_expression'] = 'stock.date'
-
-      result = self.getInventoryList(src__=src__,
-          sort_on = (('date', 'ascending'),), group_by_movement=1, **kw)
-      if src__ :
-        return result
-
-      total_inventory = 0.
-      for inventory in result:
-        if inventory['inventory'] is not None:
-          total_inventory += inventory['inventory']
-          if total_inventory < 0:
-            return inventory['date']
-
-      return None
+      return self.getNextAlertInventoryDate(reference_quantity=0, **kw)
 
     #######################################################
     # Traceability management
diff --git a/product/ERP5/mixin/builder.py b/product/ERP5/mixin/builder.py
index 0ef6fdfc9c..30be95ae81 100644
--- a/product/ERP5/mixin/builder.py
+++ b/product/ERP5/mixin/builder.py
@@ -183,6 +183,18 @@ class BuilderMixin(XMLObject, Amount, Predicate):
                                                    group_by_section=0,
                                                    **kw)
     id_count = 0
+    # min_flow and max_delay are stored on a supply line. By default
+    # we can get them through a method having the right supply type prefix
+    # like getPurchaseSupplyLineMinFlow. So we need to guess the supply prefix
+    supply_prefix = ''
+    delivery_type = self.getDeliveryPortalType()
+    portal = self.getPortalObject()
+    if delivery_type in portal.getPortalPurchaseTypeList():
+      supply_prefix = 'purchase'
+    elif delivery_type in portal.getPortalSaleTypeList():
+      supply_prefix = 'sale'
+    else:
+      supply_prefix = 'internal'
     for inventory_item in sql_list:
       if (inventory_item.inventory is not None):
         dumb_movement = inventory_item.getObject()
@@ -191,27 +203,29 @@ class BuilderMixin(XMLObject, Amount, Predicate):
                                    str(id_count))
         id_count += 1
         resource_portal_type = self.getResourcePortalType()
+        resource = portal.portal_catalog.getObject(inventory_item.resource_uid)
+        assert resource.getPortalType() == resource_portal_type
         movement.edit(
             resource=inventory_item.resource_relative_url,
             variation_category_list=dumb_movement.getVariationCategoryList(),
             destination_value=self.getDestinationValue(),
             resource_portal_type=resource_portal_type,
             destination_section_value=self.getDestinationSectionValue())
-        # We can do other test on inventory here
-        # XXX It is better if it can be sql parameters
-        #resource_portal_type = self.getResourcePortalType()
-        resource = movement.getResourceValue()
-        # FIXME: XXX Those properties are defined on a supply line !!
-        # min_flow, max_delay
-        min_flow = resource.getMinFlow(0)
-        assert resource.getPortalType() == resource_portal_type
-        if round(inventory_item.inventory, 5) < min_flow:
-          stop_date = resource.getNextNegativeInventoryDate(
+        # Get min_flow, max_delay on supply line
+        min_flow = 0
+        max_delay = 0
+        min_stock = 0
+        if supply_prefix:
+          min_flow = resource.getProperty(supply_prefix + '_supply_line_min_flow', 0)
+          max_delay = resource.getProperty(supply_prefix + '_supply_line_max_delay', 0)
+          min_stock = resource.getProperty(supply_prefix + '_supply_line_min_stock', 0)
+        if round(inventory_item.inventory, 5) < min_stock:
+          stop_date = resource.getNextAlertInventoryDate(
+                               reference_quantity=min_stock,
                                variation_text=movement.getVariationText(),
                                from_date=DateTime(),
                                **kw)
-          if stop_date is None:
-            stop_date = DateTime()
+          assert stop_date is not None
           max_delay = resource.getMaxDelay(0)
           movement.edit(
             start_date=DateTime(((stop_date-max_delay).Date())),
diff --git a/product/ERP5/tests/testOrderBuilder.py b/product/ERP5/tests/testOrderBuilder.py
index 0246c2de1c..927b1a3867 100644
--- a/product/ERP5/tests/testOrderBuilder.py
+++ b/product/ERP5/tests/testOrderBuilder.py
@@ -55,8 +55,8 @@ class TestOrderBuilderMixin(TestOrderMixin):
 
   # defaults
   decrease_quantity = 1.0
-  max_delay = 4.0
-  min_flow = 7.0
+  max_delay = 0.0
+  min_flow = 0.0
 
   def afterSetUp(self):
     """
@@ -76,7 +76,7 @@ class TestOrderBuilderMixin(TestOrderMixin):
     Sets min_flow on resource
     """
     resource = sequence.get('resource')
-    resource.edit(min_flow=self.min_flow)
+    resource.edit(purchase_supply_line_min_flow=self.min_flow)
 
   def stepFillOrderBuilder(self, sequence):
     """
@@ -169,7 +169,7 @@ class TestOrderBuilderMixin(TestOrderMixin):
     # XXX: ... and for more lines/cells too
     order_line, = order.contentValues(portal_type=self.order_line_portal_type)
     self.assertEqual(order_line.getResourceValue(), resource)
-    self.assertEqual(order_line.getTotalQuantity(), self.min_flow)
+    self.assertEqual(order_line.getTotalQuantity(), self.wanted_quantity)
 
   def stepBuildOrderBuilder(self, sequence):
     """
@@ -204,7 +204,7 @@ class TestOrderBuilderMixin(TestOrderMixin):
     packing_list = packing_list_module.newContent(
       portal_type = self.packing_list_portal_type,
       source_value = organisation,
-      start_date = self.datetime,
+      start_date = self.datetime+10,
       specialise = self.business_process,
     )
 
@@ -214,6 +214,13 @@ class TestOrderBuilderMixin(TestOrderMixin):
       quantity = self.decrease_quantity,
     )
 
+    self.decrease_quantity_matrix = {
+      'variation/%s/blue' % resource.getRelativeUrl() : 1.0,
+      'variation/%s/green' % resource.getRelativeUrl() : 2.0,
+    }
+
+    self.wanted_quantity_matrix = self.decrease_quantity_matrix.copy()
+
     packing_list_line.setVariationCategoryList(
       self.decrease_quantity_matrix.keys(),
     )
@@ -251,7 +258,7 @@ class TestOrderBuilderMixin(TestOrderMixin):
     packing_list = packing_list_module.newContent(
       portal_type = self.packing_list_portal_type,
       source_value = organisation,
-      start_date = self.datetime+14,
+      start_date = self.datetime+10,
       specialise = self.business_process,
     )
 
@@ -263,6 +270,23 @@ class TestOrderBuilderMixin(TestOrderMixin):
 
     packing_list.confirm()
 
+  def stepCreateVariatedResource(self, sequence=None, sequence_list=None, \
+                                 **kw):
+    """
+      Create a resource with variation
+    """
+    portal = self.getPortal()
+    resource_module = portal.getDefaultModule(self.resource_portal_type)
+    resource = resource_module.newContent(portal_type=self.resource_portal_type)
+    resource.edit(
+      title = "VariatedResource%s" % resource.getId(),
+      variation_base_category_list = ['variation'],
+    )
+    for color in ['blue', 'green']:
+      resource.newContent(portal_type='Product Individual Variation',
+                          id=color, title=color)
+    sequence.edit(resource=resource)
+
 class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
   """
     Test Order Builder functionality
@@ -301,14 +325,12 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
       str(self.datetime.earliestTime()
           + self.order_builder_hardcoded_time_diff))
 
-    # We add 4 days to start date to reflect delays
-    self.wanted_stop_date = self.wanted_start_date + 4
+    self.wanted_stop_date = self.wanted_start_date
 
     sequence_list = SequenceList()
     sequence_list.addSequenceString(self.common_sequence_string)
     sequence_list.play(self)
 
-  @newSimulationExpectedFailure
   def test_01a_simpleOrderBuilderVariatedResource(self, quiet=0, run=run_all_test):
     """
     Test simple Order Builder for Variated Resource
@@ -338,25 +360,17 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
 
     self.wanted_stop_date = self.wanted_start_date
 
-    self.decrease_quantity_matrix = {
-      'size/Man' : 1.0,
-      'size/Woman' : 2.0,
-    }
-
-    self.wanted_quantity_matrix = self.decrease_quantity_matrix.copy()
-
     sequence_list = SequenceList()
     sequence_list.addSequenceString(sequence_string)
     sequence_list.play(self)
 
-  @newSimulationExpectedFailure
   def test_02_maxDelayResourceOrderBuilder(self, quiet=0, run=run_all_test):
     """
     Test max_delay impact on generated order start date
     """
     if not run: return
 
-    self.max_delay = 14.0
+    self.max_delay = 4.0
 
     self.wanted_quantity = 1.0
     self.wanted_start_date = DateTime(
@@ -372,7 +386,6 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
     sequence_list.addSequenceString(self.common_sequence_string)
     sequence_list.play(self)
 
-  @newSimulationExpectedFailure
   def test_03_minFlowResourceOrderBuilder(self, quiet=0, run=run_all_test):
     """
     Test min_flow impact on generated order line quantity
@@ -390,20 +403,9 @@ class TestOrderBuilder(TestOrderBuilderMixin, ERP5TypeTestCase):
     sequence_list.addSequenceString(self.common_sequence_string)
 
     # case when min_flow > decreased_quantity
-    self.min_flow = 144.0
-    self.wanted_quantity = self.min_flow + self.decrease_quantity
-    # why to order more than needed?
-    # self.wanted_quantity = self.min_flow
-
-    sequence_list.play(self)
-
-    # case when min_flow < decreased_quantity
     self.min_flow = 15.0
-    self.decrease_quantity = 20.0
 
-    self.wanted_quantity = self.min_flow + self.decrease_quantity
-    # why to order more than needed?
-    # self.wanted_quantity = self.decreased_quantity
+    self.wanted_quantity = self.min_flow
 
     sequence_list.play(self)
 
-- 
2.30.9