1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
23 from datetime import datetime, timedelta
24 from dateutil import rrule
27 from openerp.osv import fields, osv
28 from openerp.tools.float_utils import float_compare
29 from openerp.tools.translate import _
31 from itertools import groupby
32 from operator import itemgetter
35 class resource_calendar(osv.osv):
36 _name = "resource.calendar"
37 _description = "Resource Calendar"
39 'name' : fields.char("Name", size=64, required=True),
40 'company_id' : fields.many2one('res.company', 'Company', required=False),
41 'attendance_ids' : fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
42 'manager' : fields.many2one('res.users', 'Workgroup Manager'),
45 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
48 def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
49 """Calculates the Working Total Hours based on Resource Calendar and
50 given working day (datetime object).
52 @param resource_calendar_id: resource.calendar browse record
53 @param day: datetime object
55 @return: returns the working hours (as float) men should work on the given day if is in the attendance_ids of the resource_calendar_id (i.e if that day is a working day), returns 0.0 otherwise
58 for working_day in resource_calendar_id.attendance_ids:
59 if (int(working_day.dayofweek) + 1) == day.isoweekday():
60 res += working_day.hour_to - working_day.hour_from
63 def _get_leaves(self, cr, uid, id, resource):
64 """Private Method to Calculate resource Leaves days
66 @param id: resource calendar id
67 @param resource: resource id for which leaves will ew calculated
69 @return : returns the list of dates, where resource on leave in
70 resource.calendar.leaves object (e.g.['%Y-%m-%d', '%Y-%m-%d'])
72 resource_cal_leaves = self.pool.get('resource.calendar.leaves')
74 resource_leave_ids = resource_cal_leaves.search(cr, uid, [('calendar_id','=',id), '|', ('resource_id','=',False), ('resource_id','=',resource)])
75 #res_leaves = resource_cal_leaves.read(cr, uid, resource_leave_ids, ['date_from', 'date_to'])
76 res_leaves = resource_cal_leaves.browse(cr, uid, resource_leave_ids)
78 for leave in res_leaves:
79 dtf = datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S')
80 dtt = datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S')
82 [dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
87 def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
89 Calculates the working Schedule from supplied from date to till hours
90 will be satisfied based or resource calendar id. If resource is also
91 given then it will consider the resource leave also and than will
92 calculates resource working schedule
94 @param dt_from: datetime object, start of working scheduled
95 @param hours: float, total number working hours needed scheduled from
97 @param resource : Optional Resource id, if supplied than resource leaves
98 will also taken into consideration for calculating working
100 @return : List datetime object of working schedule based on supplies
105 return [(dt_from - timedelta(hours=td), dt_from)]
106 dt_leave = self._get_leaves(cr, uid, id, resource)
111 current_hour = dt_from.hour
112 while float_compare(todo, 0, 4) and maxrecur:
113 cr.execute("select hour_from,hour_to from resource_calendar_attendance where dayofweek='%s' and calendar_id=%s order by hour_from desc", (dt_from.weekday(),id))
114 for (hour_from,hour_to) in cr.fetchall():
116 if (hour_from<current_hour) and float_compare(todo, 0, 4):
117 m = min(hour_to, current_hour)
118 if (m-hour_from)>todo:
120 dt_check = dt_from.strftime('%Y-%m-%d')
121 for leave in dt_leave:
122 if dt_check == leave:
123 dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
128 d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60))
129 d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
130 result.append((d1, d2))
131 current_hour = hour_from
132 todo -= (m-hour_from)
133 dt_from -= timedelta(days=1)
139 # def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
140 def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
142 lst.sort(key=itemgetter(key))
143 grouped = groupby(lst, itemgetter(key))
144 return dict([(k, [v for v in itr]) for k, itr in grouped])
147 cr.execute("select calendar_id, dayofweek, hour_from, hour_to from resource_calendar_attendance order by hour_from")
148 hour_res = cr.dictfetchall()
149 hours_by_cal = group(hour_res, 'calendar_id')
153 for d, hours, id in date_and_hours_by_cal:
154 dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S')
157 results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))]
160 dt_leave = self._get_leaves(cr, uid, id, resource)
164 current_hour = dt_from.hour
165 while float_compare(todo, 0, 4) and maxrecur:
166 for (hour_from,hour_to) in [(item['hour_from'], item['hour_to']) for item in hours_by_cal[id] if item['dayofweek'] == str(dt_from.weekday())]:
168 if (hour_to>current_hour) and float_compare(todo, 0, 4):
169 m = max(hour_from, current_hour)
172 dt_check = dt_from.strftime('%Y-%m-%d')
173 for leave in dt_leave:
174 if dt_check == leave:
175 dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
180 d1 = datetime(dt_from.year, dt_from.month, dt_from.day) + timedelta(hours=int(math.floor(m)), minutes=int((m%1) * 60))
181 d2 = datetime(dt_from.year, dt_from.month, dt_from.day) + timedelta(hours=int(math.floor(hour_to)), minutes=int((hour_to%1) * 60))
182 result.append((d1, d2))
183 current_hour = hour_to
184 todo -= (hour_to - m)
185 dt_from += timedelta(days=1)
188 results[(d, hours, id)] = result
191 def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
192 """Calculates Resource Working Internal Timing Based on Resource Calendar.
194 @param dt_from: start resource schedule calculation.
195 @param hours : total number of working hours to be scheduled.
196 @param resource: optional resource id, If supplied it will take care of
197 resource leave while scheduling.
198 @param byday: boolean flag bit enforce day wise scheduling
200 @return : list of scheduled working timing based on resource calendar.
202 res = self.interval_get_multi(cr, uid, [(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)], resource, byday)[(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)]
205 def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
206 """ Calculates the Total Working hours based on given start_date to
207 end_date, If resource id is supplied that it will consider the source
208 leaves also in calculating the hours.
210 @param dt_from : date start to calculate hours
211 @param dt_end : date end to calculate hours
212 @param resource: optional resource id, If given resource leave will be
215 @return : Total number of working hours based dt_from and dt_end and
216 resource if supplied.
218 return self._interval_hours_get(cr, uid, id, dt_from, dt_to, resource_id=resource)
220 def _interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource_id=False, timezone_from_uid=None, exclude_leaves=True, context=None):
221 """ Calculates the Total Working hours based on given start_date to
222 end_date, If resource id is supplied that it will consider the source
223 leaves also in calculating the hours.
225 @param dt_from : date start to calculate hours
226 @param dt_end : date end to calculate hours
227 @param resource_id: optional resource id, If given resource leave will be
229 @param timezone_from_uid: optional uid, if given we will considerer
230 working hours in that user timezone
231 @param exclude_leaves: optionnal, if set to True (default) we will exclude
232 resource leaves from working hours
233 @param context: current request context
234 @return : Total number of working hours based dt_from and dt_end and
235 resource if supplied.
237 utc_tz = pytz.timezone('UTC')
240 if timezone_from_uid:
241 users_obj = self.pool.get('res.users')
242 user_timezone = users_obj.browse(cr, uid, timezone_from_uid, context=context).partner_id.tz
245 local_tz = pytz.timezone(user_timezone)
246 except pytz.UnknownTimeZoneError:
247 pass # fallback to UTC as local timezone
249 def utc_to_local_zone(naive_datetime):
250 utc_dt = utc_tz.localize(naive_datetime, is_dst=False)
251 return utc_dt.astimezone(local_tz)
253 def float_time_convert(float_val):
254 factor = float_val < 0 and -1 or 1
256 return (factor * int(math.floor(val)), int(round((val % 1) * 60)))
258 # Get slots hours per day
259 # {day_of_week: [(8, 12), (13, 17), ...], ...}
260 hours_range_per_weekday = {}
262 cr.execute("select dayofweek, hour_from,hour_to from resource_calendar_attendance where calendar_id=%s order by hour_from", (id,))
263 for weekday, hour_from, hour_to in cr.fetchall():
264 weekday = int(weekday)
265 hours_range_per_weekday.setdefault(weekday, [])
266 hours_range_per_weekday[weekday].append((hour_from, hour_to))
268 # considering default working hours (Monday -> Friday, 8 -> 12, 13 -> 17)
269 for weekday in range(5):
270 hours_range_per_weekday[weekday] = [(8, 12), (13, 17)]
272 ## Interval between dt_from - dt_to
275 ## =============|==================|============
276 ## [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]
278 ## [ : start of range
281 ## case 1: range end before interval start (skip)
282 ## case 2: range overlap interval start (fit start to internal)
283 ## case 3: range within interval
284 ## case 4: range overlap interval end (fit end to interval)
285 ## case 5: range start after interval end (skip)
287 interval_start = utc_to_local_zone(dt_from)
288 interval_end = utc_to_local_zone(dt_to)
289 hours_timedelta = timedelta()
291 # Get leaves for requested resource
293 if exclude_leaves and id:
294 dt_leaves = set(self._get_leaves(cr, uid, id, resource=resource_id))
296 for day in rrule.rrule(rrule.DAILY, dtstart=interval_start,
297 until=interval_end+timedelta(days=1),
298 byweekday=hours_range_per_weekday.keys()):
299 if exclude_leaves and day.strftime('%Y-%m-%d') in dt_leaves:
300 # XXX: futher improve leave management to allow for partial day leave
302 for (range_from, range_to) in hours_range_per_weekday.get(day.weekday(), []):
303 range_from_hour, range_from_min = float_time_convert(range_from)
304 range_to_hour, range_to_min = float_time_convert(range_to)
305 daytime_start = local_tz.localize(day.replace(hour=range_from_hour, minute=range_from_min, second=0, tzinfo=None))
306 daytime_end = local_tz.localize(day.replace(hour=range_to_hour, minute=range_to_min, second=0, tzinfo=None))
308 # case 1 & 5: time range out of interval
309 if daytime_end < interval_start or daytime_start > interval_end:
311 # case 2 & 4: adjust start, end to fit within interval
312 daytime_start = max(daytime_start, interval_start)
313 daytime_end = min(daytime_end, interval_end)
316 hours_timedelta += (daytime_end - daytime_start)
318 # return timedelta converted to hours
319 return (hours_timedelta.days * 24.0 + hours_timedelta.seconds / 3600.0)
322 class resource_calendar_attendance(osv.osv):
323 _name = "resource.calendar.attendance"
324 _description = "Work Detail"
327 'name' : fields.char("Name", size=64, required=True),
328 'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of Week', required=True, select=True),
329 'date_from' : fields.date('Starting Date'),
330 'hour_from' : fields.float('Work from', required=True, help="Start and End time of working.", select=True),
331 'hour_to' : fields.float("Work to", required=True),
332 'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
335 _order = 'dayofweek, hour_from'
341 def hours_time_string(hours):
342 """ convert a number of hours (float) into a string with format '%H:%M' """
343 minutes = int(round(hours * 60))
344 return "%02d:%02d" % divmod(minutes, 60)
346 class resource_resource(osv.osv):
347 _name = "resource.resource"
348 _description = "Resource Detail"
350 'name' : fields.char("Name", size=64, required=True),
351 'code': fields.char('Code', size=16),
352 'active' : fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the resource record without removing it."),
353 'company_id' : fields.many2one('res.company', 'Company'),
354 'resource_type': fields.selection([('user','Human'),('material','Material')], 'Resource Type', required=True),
355 'user_id' : fields.many2one('res.users', 'User', help='Related user name for the resource to manage its access.'),
356 'time_efficiency' : fields.float('Efficiency Factor', size=8, required=True, help="This field depict the efficiency of the resource to complete tasks. e.g resource put alone on a phase of 5 days with 5 tasks assigned to him, will show a load of 100% for this phase by default, but if we put a efficiency of 200%, then his load will only be 50%."),
357 'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
360 'resource_type' : 'user',
361 'time_efficiency' : 1,
363 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
367 def copy(self, cr, uid, id, default=None, context=None):
370 if not default.get('name', False):
371 default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
372 return super(resource_resource, self).copy(cr, uid, id, default, context)
374 def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
376 Return a list of Resource Class objects for the resources allocated to the phase.
379 user_pool = self.pool.get('res.users')
380 for user in user_pool.browse(cr, uid, user_ids, context=context):
381 resource_objs[user.id] = {
387 resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
389 for resource in self.browse(cr, uid, resource_ids, context=context):
390 resource_objs[user.id]['efficiency'] = resource.time_efficiency
391 resource_cal = resource.calendar_id.id
393 leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
394 resource_objs[user.id]['vacation'] += list(leaves)
397 def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
399 Compute the vacation from the working calendar of the resource.
401 @param calendar_id : working calendar of the project
402 @param resource_id : resource working on phase/task
403 @param resource_calendar : working calendar of the resource
405 resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
408 leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
409 ('calendar_id', '=', resource_calendar),
410 ('resource_id', '=', resource_id)
413 leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
414 ('resource_id', '=', False)
416 leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
417 for i in range(len(leaves)):
418 dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
419 dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
420 no = dt_end - dt_start
421 [leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
425 def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
427 Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
428 @param calendar_id : working calendar of the project
431 # Calendar is not specified: working days: 24/7
432 return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'),
433 ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')]
434 resource_attendance_pool = self.pool.get('resource.calendar.attendance')
435 time_range = "8:00-8:00"
437 week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
442 week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context)
443 weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context)
444 # Convert time formats into appropriate format required
445 # and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')]
449 if week_days.get(week['dayofweek'],False):
450 day = week_days[week['dayofweek']]
451 wk_days[week['dayofweek']] = week_days[week['dayofweek']]
453 raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!'))
454 hour_from_str = hours_time_string(week['hour_from'])
455 hour_to_str = hours_time_string(week['hour_to'])
456 res_str = hour_from_str + '-' + hour_to_str
457 wktime_list.append((day, res_str))
458 # Convert into format like [('mon', '8:00-12:00', '13:00-18:00')]
459 for item in wktime_list:
460 if wk_time.has_key(item[0]):
461 wk_time[item[0]].append(item[1])
463 wk_time[item[0]] = [item[0]]
464 wk_time[item[0]].append(item[1])
465 for k,v in wk_time.items():
466 wktime_cal.append(tuple(v))
467 # Add for the non-working days like: [('sat, sun', '8:00-8:00')]
468 for k, v in wk_days.items():
469 if week_days.has_key(k):
471 for v in week_days.itervalues():
472 non_working += v + ','
474 wktime_cal.append((non_working[:-1], time_range))
478 class resource_calendar_leaves(osv.osv):
479 _name = "resource.calendar.leaves"
480 _description = "Leave Detail"
482 'name' : fields.char("Name", size=64),
483 'company_id' : fields.related('calendar_id','company_id',type='many2one',relation='res.company',string="Company", store=True, readonly=True),
484 'calendar_id' : fields.many2one("resource.calendar", "Working Time"),
485 'date_from' : fields.datetime('Start Date', required=True),
486 'date_to' : fields.datetime('End Date', required=True),
487 'resource_id' : fields.many2one("resource.resource", "Resource", help="If empty, this is a generic holiday for the company. If a resource is set, the holiday/leave is only for this resource"),
490 def check_dates(self, cr, uid, ids, context=None):
491 leave = self.read(cr, uid, ids[0], ['date_from', 'date_to'])
492 if leave['date_from'] and leave['date_to']:
493 if leave['date_from'] > leave['date_to']:
498 (check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
501 def onchange_resource(self, cr, uid, ids, resource, context=None):
504 resource_pool = self.pool.get('resource.resource')
505 result['calendar_id'] = resource_pool.browse(cr, uid, resource, context=context).calendar_id.id
506 return {'value': result}
507 return {'value': {'calendar_id': []}}
510 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: