From 844918e814c5bf8d0aef9fb1445f66ed4ec2f944 Mon Sep 17 00:00:00 2001
From: Julien Muchembled <jm@nexedi.com>
Date: Fri, 8 Jan 2010 03:25:17 +0000
Subject: [PATCH] Fix DateUtils.atTheEndOfPeriod and an infinite loop in some
 timezones

Contrary to Europe/Paris, Zope performs automatic DST calculation in US/Eastern
timezone, which caused infinite loop in Alarm.getNextPeriodicalDate.

Reenable testOpenOrder.testPeriodicityDateList (still failing though),
and create a testOpenOrder.testPeriodicityDateListUniversal to show that it
works with UTC times.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@31657 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5/Document/Alarm.py          | 45 +++++---------
 product/ERP5/tests/testOpenOrder.py     | 83 +++++++++++++------------
 product/ERP5Type/DateUtils.py           | 11 +---
 product/ERP5Type/tests/testDateUtils.py | 18 ++++++
 4 files changed, 79 insertions(+), 78 deletions(-)

diff --git a/product/ERP5/Document/Alarm.py b/product/ERP5/Document/Alarm.py
index ca60739173..6d3002882e 100644
--- a/product/ERP5/Document/Alarm.py
+++ b/product/ERP5/Document/Alarm.py
@@ -35,7 +35,7 @@ from Products.ERP5Type.XMLObject import XMLObject
 from Acquisition import aq_base
 from DateTime import DateTime
 from Products.ERP5Type.Message import Message
-from Products.ERP5Type.DateUtils import addToDate
+from Products.ERP5Type.DateUtils import addToDate, atTheEndOfPeriod
 from Products.ERP5Security.ERP5UserManager import SUPER_USER
 from AccessControl.SecurityManagement import getSecurityManager, \
             setSecurityManager, newSecurityManager
@@ -158,40 +158,23 @@ class PeriodicityMixin:
           or (periodicity_stop_date is not None \
               and next_start_date >= periodicity_stop_date):
       return None
-    else:
-      # Make sure the old date is not too far away
-      day_count = int(current_date - next_start_date)
-      next_start_date = next_start_date + day_count
 
     previous_date = next_start_date
-    next_start_date = addToDate(next_start_date, minute=1)
+    next_start_date = max(addToDate(next_start_date, minute=1), current_date)
     while 1:
-      validate_minute = self._validateMinute(next_start_date, previous_date)
-      validate_hour = self._validateHour(next_start_date)
-      validate_day = self._validateDay(next_start_date)
-      validate_week = self._validateWeek(next_start_date)
-      validate_month = self._validateMonth(next_start_date)
-      if (next_start_date >= current_date \
-          and validate_minute and validate_hour and validate_day \
-          and validate_week and validate_month):
-        break
+      if not self._validateMonth(next_start_date):
+        next_start_date = atTheEndOfPeriod(next_start_date, 'month')
+      elif not (self._validateDay(next_start_date) and
+                self._validateWeek(next_start_date)):
+        next_start_date = atTheEndOfPeriod(next_start_date, 'day')
+      elif not self._validateMinute(next_start_date, previous_date):
+        next_start_date = addToDate(next_start_date, minute=1)
+      elif not self._validateHour(next_start_date):
+        next_start_date = addToDate(next_start_date, hour=1)
       else:
-        if not(validate_minute):
-          next_start_date = addToDate(next_start_date, minute=1)
-        else:
-          if not(validate_hour):
-            next_start_date = addToDate(next_start_date, hour=1)
-          else:
-            if not(validate_day and validate_week and validate_month):
-              # We have to reset hours and minutes in order to make sure
-              # we will start at the beginning of the next day
-              next_start_date = DateTime(next_start_date.Date() + ' 00:00:00 %s' % next_start_date.timezone())
-              next_start_date = addToDate(next_start_date, day=1)
-            else:
-              # Everything is right, but the date is still not bigger
-              # than the current date, so we must continue
-              next_start_date = addToDate(next_start_date, minute=1)
-    return next_start_date
+        parts = list(next_start_date.parts())
+        parts[5] = previous_date.second() # XXX keep old behaviour
+        return DateTime(*parts)
 
   # XXX May be we should create a Date class for following methods ???
   security.declareProtected(Permissions.AccessContentsInformation, 'getWeekDayList')
diff --git a/product/ERP5/tests/testOpenOrder.py b/product/ERP5/tests/testOpenOrder.py
index 3370c62ced..a5ad8259a6 100644
--- a/product/ERP5/tests/testOpenOrder.py
+++ b/product/ERP5/tests/testOpenOrder.py
@@ -172,59 +172,64 @@ class TestOpenOrder(ERP5TypeTestCase):
     transaction.commit()
     self.tic()
 
-  def testPeriodicityDateList(self):
+  def testPeriodicityDateList(self, timezone=None):
     """
     Make sure that periodicity line can generate correct schedule.
     """
-    self.fail('Test disabled because it freezes')
+    #self.fail('Test disabled because it freezes')
+    def D(yr, mo, dy, hr=0, mn=0, sc=0):
+      return DateTime(yr, mo, dy, hr, mn, sc, timezone)
     # This across Summer time period, if server's timezone uses it.
     self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.internet_connection_periodicity_line.getDatePeriodList(
-      DateTime(2008,1,15), DateTime(2008,12,1)),
-                     [(DateTime(2008,2,1,0,1), DateTime(2008,2,29)),
-                      (DateTime(2008,3,1,0,1), DateTime(2008,3,31)),
-                      (DateTime(2008,4,1,0,1), DateTime(2008,4,30)),
-                      (DateTime(2008,5,1,0,1), DateTime(2008,5,31)),
-                      (DateTime(2008,6,1,0,1), DateTime(2008,6,30)),
-                      (DateTime(2008,7,1,0,1), DateTime(2008,7,31)),
-                      (DateTime(2008,8,1,0,1), DateTime(2008,8,31)),
-                      (DateTime(2008,9,1,0,1), DateTime(2008,9,30)),
-                      (DateTime(2008,10,1,0,1), DateTime(2008,10,31)),
-                      (DateTime(2008,11,1,0,1), DateTime(2008,11,30)),
+      D(2008,1,15), D(2008,12,1)),
+                    [(D(2008,2,1,0,1), DateTime(2008,2,29)),
+                      (D(2008,3,1,0,1), DateTime(2008,3,31)),
+                      (D(2008,4,1,0,1), DateTime(2008,4,30)),
+                      (D(2008,5,1,0,1), DateTime(2008,5,31)),
+                      (D(2008,6,1,0,1), DateTime(2008,6,30)),
+                      (D(2008,7,1,0,1), DateTime(2008,7,31)),
+                      (D(2008,8,1,0,1), DateTime(2008,8,31)),
+                      (D(2008,9,1,0,1), DateTime(2008,9,30)),
+                      (D(2008,10,1,0,1), DateTime(2008,10,31)),
+                      (D(2008,11,1,0,1), DateTime(2008,11,30)),
                       ])
-    
+
     self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.bread_periodicity_line.getDatePeriodList(
-      DateTime(2008,2,26), DateTime(2008,3,5)),
-                     [(DateTime(2008,2,26,6,0), DateTime(2008,2,26,6,0)),
-                      (DateTime(2008,2,26,12,0), DateTime(2008,2,26,12,0)),
-                      (DateTime(2008,2,27,6,0), DateTime(2008,2,27,6,0)),
-                      (DateTime(2008,2,27,12,0), DateTime(2008,2,27,12,0)),
-                      (DateTime(2008,2,28,6,0), DateTime(2008,2,28,6,0)),
-                      (DateTime(2008,2,28,12,0), DateTime(2008,2,28,12,0)),
-                      (DateTime(2008,2,29,6,0), DateTime(2008,2,29,6,0)),
-                      (DateTime(2008,2,29,12,0), DateTime(2008,2,29,12,0)),
-                      (DateTime(2008,3,1,6,0), DateTime(2008,3,1,6,0)),
-                      (DateTime(2008,3,1,12,0), DateTime(2008,3,1,12,0)),
-                      (DateTime(2008,3,3,6,0), DateTime(2008,3,3,6,0)),
-                      (DateTime(2008,3,3,12,0), DateTime(2008,3,3,12,0)),
-                      (DateTime(2008,3,4,6,0), DateTime(2008,3,4,6,0)),
-                      (DateTime(2008,3,4,12,0), DateTime(2008,3,4,12,0)),
+      D(2008,2,26), D(2008,3,5)),
+                    [(D(2008,2,26,6,0), D(2008,2,26,6,0)),
+                      (D(2008,2,26,12,0), D(2008,2,26,12,0)),
+                      (D(2008,2,27,6,0), D(2008,2,27,6,0)),
+                      (D(2008,2,27,12,0), D(2008,2,27,12,0)),
+                      (D(2008,2,28,6,0), D(2008,2,28,6,0)),
+                      (D(2008,2,28,12,0), D(2008,2,28,12,0)),
+                      (D(2008,2,29,6,0), D(2008,2,29,6,0)),
+                      (D(2008,2,29,12,0), D(2008,2,29,12,0)),
+                      (D(2008,3,1,6,0), D(2008,3,1,6,0)),
+                      (D(2008,3,1,12,0), D(2008,3,1,12,0)),
+                      (D(2008,3,3,6,0), D(2008,3,3,6,0)),
+                      (D(2008,3,3,12,0), D(2008,3,3,12,0)),
+                      (D(2008,3,4,6,0), D(2008,3,4,6,0)),
+                      (D(2008,3,4,12,0), D(2008,3,4,12,0)),
                       ])
 
     self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.water_periodicity_line.getDatePeriodList(
-      DateTime(2008,2,16), DateTime(2008,4,15)),
-                     [(DateTime(2008,2,18,10,0), DateTime(2008,3,3,10,0)),
-                      (DateTime(2008,3,3,10,0), DateTime(2008,3,17,10,0)),
-                      (DateTime(2008,3,17,10,0), DateTime(2008,3,31,10,0)),
-                      (DateTime(2008,3,31,10,0), DateTime(2008,4,14,10,0)),
-                      (DateTime(2008,4,14,10,0), DateTime(2008,4,28,10,0)),
+      D(2008,2,16), D(2008,4,15)),
+                    [(D(2008,2,18,10,0), D(2008,3,3,10,0)),
+                      (D(2008,3,3,10,0), D(2008,3,17,10,0)),
+                      (D(2008,3,17,10,0), D(2008,3,31,10,0)),
+                      (D(2008,3,31,10,0), D(2008,4,14,10,0)),
+                      (D(2008,4,14,10,0), D(2008,4,28,10,0)),
                       ])
     self.assertEqual(self.portal.sale_trade_condition_module.main_trade_condition.training_periodicity_line.getDatePeriodList(
-      DateTime(2008,2,16), DateTime(2008,3,6)),
-                     [(DateTime(2008,2,18,10,0), DateTime(2008,2,19,10,0)),
-                      (DateTime(2008,2,25,10,0), DateTime(2008,2,26,10,0)),
-                      (DateTime(2008,3,3,10,0), DateTime(2008,3,4,10,0)),
+      D(2008,2,16), D(2008,3,6)),
+                    [(D(2008,2,18,10,0), D(2008,2,19,10,0)),
+                      (D(2008,2,25,10,0), D(2008,2,26,10,0)),
+                      (D(2008,3,3,10,0), D(2008,3,4,10,0)),
                       ])
 
+  def testPeriodicityDateListUniversal(self):
+    self.testPeriodicityDateList('Universal')
+
   def testOpenOrderRule(self):
     """
     Make sure that Open Order Rule can generate simulation movements by
diff --git a/product/ERP5Type/DateUtils.py b/product/ERP5Type/DateUtils.py
index 22f6d4ec29..3d07262d26 100644
--- a/product/ERP5Type/DateUtils.py
+++ b/product/ERP5Type/DateUtils.py
@@ -506,21 +506,16 @@ def atTheEndOfPeriod(date, period):
   If timezone is Universal, strftime('%Z') return empty string
   and TimeZone is replaced by local zone, 
   so date formating is manualy rendered.
-  XXXSunday is hardcoded
   """
   if period == 'year':
     end = addToDate(DateTime('%s/01/01 00:00:00 %s' % (date.year(), date.timezone())), **{period:1})
   elif period == 'month':
     end = addToDate(DateTime('%s/%s/01 00:00:00 %s' % (date.year(), zfill(date.month(), 2), date.timezone())), **{period:1})
   elif period == 'day':
-    end = addToDate(DateTime('%s/%s/%s 00:00:00 %s' % (date.year(), zfill(date.month(), 2), zfill(date.day(), 2), date.timezone())), **{period:1})
+    end = addToDate(date.earliestTime(), hour=36).earliestTime()
   elif period == 'week':
-    day_of_week = date.strftime('%A')
-    end = DateTime('%s/%s/%s 00:00:00 %s' % (date.year(), zfill(date.month(), 2), zfill(date.day(), 2), date.timezone()))
-    while day_of_week != 'Sunday':
-      end = addToDate(end, day=1)
-      day_of_week = end.strftime('%A')
-    end = addToDate(end, day=1)
+    end = atTheEndOfPeriod(date, 'day')
+    end = addToDate(end, day=(1-end.dow()) % 7)
   else:
     raise NotImplementedError, 'Period "%s" not Handled yet' % period
   return end
diff --git a/product/ERP5Type/tests/testDateUtils.py b/product/ERP5Type/tests/testDateUtils.py
index 55a47d24f2..72baadb258 100644
--- a/product/ERP5Type/tests/testDateUtils.py
+++ b/product/ERP5Type/tests/testDateUtils.py
@@ -167,6 +167,24 @@ class TestDateUtils(unittest.TestCase):
     self.assertEqual(atTheEndOfPeriod(date, 'month').pCommonZ(), 'Feb. 1, 2008 12:00 am Universal')
     self.assertEqual(atTheEndOfPeriod(date, 'week').pCommonZ(), 'Jan. 7, 2008 12:00 am Universal')
     self.assertEqual(atTheEndOfPeriod(date, 'day').pCommonZ(), 'Jan. 2, 2008 12:00 am Universal')
+    # Switch to summer time
+    self.assertEqual('Apr. 6, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/04/05 23:59:59 US/Eastern'), 'day').pCommonZ())
+    self.assertEqual('Apr. 7, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/04/06 00:00:00 US/Eastern'), 'day').pCommonZ())
+    self.assertEqual('Apr. 7, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/04/06 23:59:59 US/Eastern'), 'day').pCommonZ())
+    self.assertEqual('May 1, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/04/01 US/Eastern'), 'month').pCommonZ())
+    # Switch to winter time
+    self.assertEqual('Oct. 26, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/10/25 23:59:59 US/Eastern'), 'day').pCommonZ())
+    self.assertEqual('Oct. 27, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/10/26 00:00:00 US/Eastern'), 'day').pCommonZ())
+    self.assertEqual('Oct. 27, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/10/26 23:59:59 US/Eastern'), 'day').pCommonZ())
+    self.assertEqual('Nov. 1, 2008 12:00 am US/Eastern',
+      atTheEndOfPeriod(DateTime('2008/10/01 US/Eastern'), 'month').pCommonZ())
 
 def test_suite():
   suite = unittest.TestSuite()
-- 
2.30.9