Merge lp:~openerp-dev/openobject-addons/6.1-opw-575797-odo into lp:openobject-addons/6.1

Proposed by Olivier Dony (Odoo)
Status: Work in progress
Proposed branch: lp:~openerp-dev/openobject-addons/6.1-opw-575797-odo
Merge into: lp:openobject-addons/6.1
Diff against target: 168 lines (+63/-10) (has conflicts)
3 files modified
project/project.py (+9/-2)
project_long_term/project_long_term.py (+17/-5)
resource/resource.py (+37/-3)
Text conflict in resource/resource.py
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/6.1-opw-575797-odo
Reviewer Review Type Date Requested Status
Raphael Collet (OpenERP) Pending
Review via email: mp+138736@code.launchpad.net

Description of the change

[FIX] OPW 575797: resource,project: fix erroneous scheduling due to timezone issues

The phases and tasks scheduling is based on the
working time specification, which only contains
days of week and start-stop hours. It only makes
sense when taken in the timezone of the user,
not the server-side timezone (always UTC).
The scheduling is more intuitive if we avoid
to compute it on UTC-converted hours (which
would give very strange working hours in some
places, like 23PM-8AM). Therefore to avoid
more mistakes we convert the constraint timestamps
to the user timezone before scheduling tasks
and phases, and then back to UTC when storing
the computed result

To post a comment you must log in.
Revision history for this message
Olivier Dony (Odoo) (odo-openerp) wrote :

See also related bug 932584

Unmerged revisions

6851. By Olivier Dony (Odoo)

[FIX] OPW 575797: resource,project: fix erroneous scheduling due to timezone issues

The phases and tasks scheduling is based on the
working time specification, which only contains
days of week and start-stop hours. It only makes
sense when taken in the timezone of the user,
not the server-side timezone (always UTC).
The scheduling is more intuitive if we avoid
to compute it on UTC-converted hours (which
would give very strange working hours in some
places, like 23PM-8AM). Therefore to avoid
more mistakes we convert the constraint timestamps
to the user timezone before scheduling tasks
and phases, and then back to UTC when storing
the computed result

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'project/project.py'
2--- project/project.py 2012-08-29 10:56:45 +0000
3+++ project/project.py 2012-12-07 13:55:25 +0000
4@@ -20,12 +20,14 @@
5 ##############################################################################
6
7 from lxml import etree
8+import logging
9 import time
10 from datetime import datetime, date
11
12 from tools.translate import _
13 from osv import fields, osv
14 from openerp.addons.resource.faces import task as Task
15+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTFMT
16
17 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
18 #class project_project(osv.osv):
19@@ -436,9 +438,14 @@
20
21 p = getattr(project_gantt, 'Task_%d' % (task.id,))
22
23+ # /!\ Project scheduling is computed based on user timezone, hence we must convert
24+ # back to UTC when saving
25+ resource = self.pool.get('resource.resource')
26+ start_tstamp = resource._user_tstamp_to_resource_tstamp(cr, uid, p.start.strftime(DTFMT), context=context)
27+ end_tstamp = resource._user_tstamp_to_resource_tstamp(cr, uid, p.end.strftime(DTFMT), context=context)
28 self.pool.get('project.task').write(cr, uid, [task.id], {
29- 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
30- 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
31+ 'date_start': start_tstamp,
32+ 'date_end': end_tstamp
33 }, context=context)
34 if (not task.user_id) and (p.booked_resource):
35 self.pool.get('project.task').write(cr, uid, [task.id], {
36
37=== modified file 'project_long_term/project_long_term.py'
38--- project_long_term/project_long_term.py 2012-01-31 13:36:57 +0000
39+++ project_long_term/project_long_term.py 2012-12-07 13:55:25 +0000
40@@ -23,6 +23,7 @@
41 from tools.translate import _
42 from osv import fields, osv
43 from openerp.addons.resource.faces import task as Task
44+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTFMT
45
46 class project_phase(osv.osv):
47 _name = "project.phase"
48@@ -181,7 +182,12 @@
49 effort = \"%s\"''' % (phase.id, duration)
50 start = []
51 if phase.constraint_date_start:
52- start.append('datetime.datetime.strptime("'+str(phase.constraint_date_start)+'", "%Y-%m-%d %H:%M:%S")')
53+ # /!\ Project scheduling is based on working time expressed in user timezone, so we must convert
54+ # timestamps before computing
55+ start_tstamp = self.pool.get('resource.resource')._resource_tstamp_to_user_tstamp(cr, uid,
56+ phase.constraint_date_start,
57+ context=context)
58+ start.append('datetime.datetime.strptime(%r, %r)' % (start_tstamp, DTFMT))
59 for previous_phase in phase.previous_phase_ids:
60 start.append("up.Phase_%s.end" % (previous_phase.id,))
61 if start:
62@@ -218,6 +224,7 @@
63 _columns = {
64 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"),
65 }
66+
67 def schedule_phases(self, cr, uid, ids, context=None):
68 context = context or {}
69 if type(ids) in (long, int,):
70@@ -245,16 +252,21 @@
71 context=context
72 )
73
74+ # /!\ Project scheduling is computed based on user timezone, hence we must convert
75+ # back to UTC when saving
76+ resource = self.pool.get('resource.resource')
77+ start_tstamp = resource._user_tstamp_to_resource_tstamp(cr, uid, p.start.strftime(DTFMT), context=context)
78+ end_tstamp = resource._user_tstamp_to_resource_tstamp(cr, uid, p.end.strftime(DTFMT), context=context)
79 for r in p.booked_resource:
80 self.pool.get('project.user.allocation').create(cr, uid, {
81 'user_id': int(r.name[5:]),
82 'phase_id': phase.id,
83- 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
84- 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
85+ 'date_start': start_tstamp,
86+ 'date_end': end_tstamp,
87 }, context=context)
88 self.pool.get('project.phase').write(cr, uid, [phase.id], {
89- 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
90- 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
91+ 'date_start': start_tstamp,
92+ 'date_end': end_tstamp,
93 }, context=context)
94 return True
95 project()
96
97=== modified file 'resource/resource.py'
98--- resource/resource.py 2012-10-30 19:38:23 +0000
99+++ resource/resource.py 2012-12-07 13:55:25 +0000
100@@ -21,7 +21,11 @@
101
102 import pytz
103 from datetime import datetime, timedelta
104+<<<<<<< TREE
105 from dateutil import rrule
106+=======
107+import logging
108+>>>>>>> MERGE-SOURCE
109 import math
110 from faces import *
111 from osv import fields, osv
112@@ -29,7 +33,13 @@
113
114 from itertools import groupby
115 from operator import itemgetter
116-
117+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT as DTFMT
118+
119+<<<<<<< TREE
120+=======
121+_logger = logging.getLogger(__name__)
122+
123+>>>>>>> MERGE-SOURCE
124 class resource_calendar(osv.osv):
125 _name = "resource.calendar"
126 _description = "Resource Calendar"
127@@ -397,6 +407,25 @@
128 resource_objs[user.id]['vacation'] += list(leaves)
129 return resource_objs
130
131+ def _resource_tstamp_to_user_tstamp(self, cr, uid, timestamp, context=None):
132+ if isinstance(timestamp, basestring):
133+ timestamp = datetime.strptime(timestamp, DTFMT)
134+ timestamp = fields.datetime.context_timestamp(cr, uid, timestamp, context=context)
135+ return datetime.strftime(timestamp, DTFMT)
136+
137+ def _user_tstamp_to_resource_tstamp(self, cr, uid, timestamp, context=None):
138+ if isinstance(timestamp, basestring):
139+ timestamp = datetime.strptime(timestamp, DTFMT)
140+ if context and context.get('tz'):
141+ try:
142+ import pytz
143+ user_tz = pytz.timezone(context['tz'])
144+ user_tstamp = user_tz.localize(timestamp, is_dst=True)
145+ timestamp = user_tstamp.astimezone(pytz.utc)
146+ except Exception:
147+ _logger.debug("failed to convert context/client-specific timestamp to UTC", exc_info=True)
148+ return datetime.strftime(timestamp, DTFMT)
149+
150 def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
151 """
152 Compute the vacation from the working calendar of the resource.
153@@ -418,8 +447,13 @@
154 ], context=context)
155 leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
156 for i in range(len(leaves)):
157- dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
158- dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
159+ # /!\ Vacations are returned as a list of leave dates, and
160+ # because these are to be used during resource scheduling and compared
161+ # with working times, they must be converted to the user timezone first
162+ dt_start = self._resource_tstamp_to_user_tstamp(cr, uid, leaves[i]['date_from'], context=context)
163+ dt_end = self._resource_tstamp_to_user_tstamp(cr, uid, leaves[i]['date_to'], context=context)
164+ dt_start = datetime.strptime(dt_start, DTFMT)
165+ dt_end = datetime.strptime(dt_end, DTFMT)
166 no = dt_end - dt_start
167 [leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
168 leave_list.sort()