1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-TODAY OpenERP SA (http://www.openerp.com)
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 dateutil import rrule
24 from dateutil.relativedelta import relativedelta
25 from operator import itemgetter
27 from openerp import tools
28 from openerp.osv import fields, osv
29 from openerp.tools.float_utils import float_compare
30 from openerp.tools.translate import _
32 class resource_calendar(osv.osv):
33 """ Calendar model for a resource. It has
35 - attendance_ids: list of resource.calendar.attendance that are a working
36 interval in a given weekday.
37 - leave_ids: list of leaves linked to this calendar. A leave can be general
38 or linked to a specific resource, depending on its resource_id.
40 All methods in this class use intervals. An interval is a tuple holding
41 (begin_datetime, end_datetime). A list of intervals is therefore a list of
42 tuples, holding several intervals of work or leaves. """
43 _name = "resource.calendar"
44 _description = "Resource Calendar"
47 'name': fields.char("Name", required=True),
48 'company_id': fields.many2one('res.company', 'Company', required=False),
49 'attendance_ids': fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time', copy=True),
50 'manager': fields.many2one('res.users', 'Workgroup Manager'),
51 'leave_ids': fields.one2many(
52 'resource.calendar.leaves', 'calendar_id', 'Leaves',
57 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
60 # --------------------------------------------------
62 # --------------------------------------------------
64 def interval_clean(self, intervals):
65 """ Utility method that sorts and removes overlapping inside datetime
66 intervals. The intervals are sorted based on increasing starting datetime.
67 Overlapping intervals are merged into a single one.
69 :param list intervals: list of intervals; each interval is a tuple
70 (datetime_from, datetime_to)
71 :return list cleaned: list of sorted intervals without overlap """
72 intervals = sorted(intervals, key=itemgetter(0)) # sort on first datetime
74 working_interval = None
76 current_interval = intervals.pop(0)
77 if not working_interval: # init
78 working_interval = [current_interval[0], current_interval[1]]
79 elif working_interval[1] < current_interval[0]: # interval is disjoint
80 cleaned.append(tuple(working_interval))
81 working_interval = [current_interval[0], current_interval[1]]
82 elif working_interval[1] < current_interval[1]: # union of greater intervals
83 working_interval[1] = current_interval[1]
84 if working_interval: # handle void lists
85 cleaned.append(tuple(working_interval))
88 def interval_remove_leaves(self, interval, leave_intervals):
89 """ Utility method that remove leave intervals from a base interval:
91 - clean the leave intervals, to have an ordered list of not-overlapping
93 - initiate the current interval to be the base interval
94 - for each leave interval:
96 - finishing before the current interval: skip, go to next
97 - beginning after the current interval: skip and get out of the loop
98 because we are outside range (leaves are ordered)
99 - beginning within the current interval: close the current interval
100 and begin a new current interval that begins at the end of the leave
102 - ending within the current interval: update the current interval begin
103 to match the leave interval ending
105 :param tuple interval: a tuple (beginning datetime, ending datetime) that
106 is the base interval from which the leave intervals
108 :param list leave_intervals: a list of tuples (beginning datetime, ending datetime)
109 that are intervals to remove from the base interval
110 :return list intervals: a list of tuples (begin datetime, end datetime)
111 that are the remaining valid intervals """
114 if leave_intervals is None:
117 leave_intervals = self.interval_clean(leave_intervals)
118 current_interval = [interval[0], interval[1]]
119 for leave in leave_intervals:
120 if leave[1] <= current_interval[0]:
122 if leave[0] >= current_interval[1]:
124 if current_interval[0] < leave[0] < current_interval[1]:
125 current_interval[1] = leave[0]
126 intervals.append((current_interval[0], current_interval[1]))
127 current_interval = [leave[1], interval[1]]
128 # if current_interval[0] <= leave[1] <= current_interval[1]:
129 if current_interval[0] <= leave[1]:
130 current_interval[0] = leave[1]
131 if current_interval and current_interval[0] < interval[1]: # remove intervals moved outside base interval due to leaves
132 intervals.append((current_interval[0], current_interval[1]))
135 def interval_schedule_hours(self, intervals, hour, remove_at_end=True):
136 """ Schedule hours in intervals. The last matching interval is truncated
137 to match the specified hours.
139 It is possible to truncate the last interval at its beginning or ending.
140 However this does nothing on the given interval order that should be
141 submitted accordingly.
143 :param list intervals: a list of tuples (beginning datetime, ending datetime)
144 :param int/float hours: number of hours to schedule. It will be converted
145 into a timedelta, but should be submitted as an
147 :param boolean remove_at_end: remove extra hours at the end of the last
148 matching interval. Otherwise, do it at the
151 :return list results: a list of intervals. If the number of hours to schedule
152 is greater than the possible scheduling in the intervals, no extra-scheduling
153 is done, and results == intervals. """
155 res = datetime.timedelta()
156 limit = datetime.timedelta(hours=hour)
157 for interval in intervals:
158 res += interval[1] - interval[0]
159 if res > limit and remove_at_end:
160 interval = (interval[0], interval[1] + relativedelta(seconds=seconds(limit-res)))
162 interval = (interval[0] + relativedelta(seconds=seconds(res-limit)), interval[1])
163 results.append(interval)
168 # --------------------------------------------------
169 # Date and hours computation
170 # --------------------------------------------------
172 def get_attendances_for_weekdays(self, cr, uid, id, weekdays, context=None):
173 """ Given a list of weekdays, return matching resource.calendar.attendance"""
174 calendar = self.browse(cr, uid, id, context=None)
175 return [att for att in calendar.attendance_ids if int(att.dayofweek) in weekdays]
177 def get_weekdays(self, cr, uid, id, default_weekdays=None, context=None):
178 """ Return the list of weekdays that contain at least one working interval.
179 If no id is given (no calendar), return default weekdays. """
181 return default_weekdays if default_weekdays is not None else [0, 1, 2, 3, 4]
182 calendar = self.browse(cr, uid, id, context=None)
184 for attendance in calendar.attendance_ids:
185 weekdays.add(int(attendance.dayofweek))
186 return list(weekdays)
188 def get_next_day(self, cr, uid, id, day_date, context=None):
189 """ Get following date of day_date, based on resource.calendar. If no
190 calendar is provided, just return the next day.
192 :param int id: id of a resource.calendar. If not given, simply add one day
193 to the submitted date.
194 :param date day_date: current day as a date
196 :return date: next day of calendar, or just next day """
198 return day_date + relativedelta(days=1)
199 weekdays = self.get_weekdays(cr, uid, id, context)
202 for weekday in weekdays:
203 if weekday > day_date.weekday():
207 new_index = (base_index + 1) % len(weekdays)
208 days = (weekdays[new_index] - day_date.weekday())
212 return day_date + relativedelta(days=days)
214 def get_previous_day(self, cr, uid, id, day_date, context=None):
215 """ Get previous date of day_date, based on resource.calendar. If no
216 calendar is provided, just return the previous day.
218 :param int id: id of a resource.calendar. If not given, simply remove
219 one day from the submitted date.
220 :param date day_date: current day as a date
222 :return date: previous day of calendar, or just previous day """
224 return day_date + relativedelta(days=-1)
225 weekdays = self.get_weekdays(cr, uid, id, context)
229 for weekday in weekdays:
230 if weekday < day_date.weekday():
234 new_index = (base_index + 1) % len(weekdays)
235 days = (weekdays[new_index] - day_date.weekday())
239 return day_date + relativedelta(days=days)
241 def get_leave_intervals(self, cr, uid, id, resource_id=None,
242 start_datetime=None, end_datetime=None,
244 """Get the leaves of the calendar. Leaves can be filtered on the resource,
245 the start datetime or the end datetime.
247 :param int resource_id: the id of the resource to take into account when
248 computing the leaves. If not set, only general
249 leaves are computed. If set, generic and
250 specific leaves are computed.
251 :param datetime start_datetime: if provided, do not take into account leaves
252 ending before this date.
253 :param datetime end_datetime: if provided, do not take into account leaves
254 beginning after this date.
256 :return list leaves: list of tuples (start_datetime, end_datetime) of
259 resource_calendar = self.browse(cr, uid, id, context=context)
261 for leave in resource_calendar.leave_ids:
262 if leave.resource_id and not resource_id == leave.resource_id.id:
264 date_from = datetime.datetime.strptime(leave.date_from, tools.DEFAULT_SERVER_DATETIME_FORMAT)
265 if end_datetime and date_from > end_datetime:
267 date_to = datetime.datetime.strptime(leave.date_to, tools.DEFAULT_SERVER_DATETIME_FORMAT)
268 if start_datetime and date_to < start_datetime:
270 leaves.append((date_from, date_to))
273 def get_working_intervals_of_day(self, cr, uid, id, start_dt=None, end_dt=None,
274 leaves=None, compute_leaves=False, resource_id=None,
275 default_interval=None, context=None):
276 """ Get the working intervals of the day based on calendar. This method
277 handle leaves that come directly from the leaves parameter or can be computed.
279 :param int id: resource.calendar id; take the first one if is a list
280 :param datetime start_dt: datetime object that is the beginning hours
281 for the working intervals computation; any
282 working interval beginning before start_dt
283 will be truncated. If not set, set to end_dt
284 or today() if no end_dt at 00.00.00.
285 :param datetime end_dt: datetime object that is the ending hour
286 for the working intervals computation; any
287 working interval ending after end_dt
288 will be truncated. If not set, set to start_dt()
290 :param list leaves: a list of tuples(start_datetime, end_datetime) that
292 :param boolean compute_leaves: if set and if leaves is None, compute the
293 leaves based on calendar and resource.
294 If leaves is None and compute_leaves false
295 no leaves are taken into account.
296 :param int resource_id: the id of the resource to take into account when
297 computing the leaves. If not set, only general
298 leaves are computed. If set, generic and
299 specific leaves are computed.
300 :param tuple default_interval: if no id, try to return a default working
301 day using default_interval[0] as beginning
302 hour, and default_interval[1] as ending hour.
303 Example: default_interval = (8, 16).
304 Otherwise, a void list of working intervals
305 is returned when id is None.
307 :return list intervals: a list of tuples (start_datetime, end_datetime)
308 of work intervals """
309 if isinstance(id, (list, tuple)):
312 # Computes start_dt, end_dt (with default values if not set) + off-interval work limits
314 if start_dt is None and end_dt is not None:
315 start_dt = end_dt.replace(hour=0, minute=0, second=0)
316 elif start_dt is None:
317 start_dt = datetime.datetime.now().replace(hour=0, minute=0, second=0)
319 work_limits.append((start_dt.replace(hour=0, minute=0, second=0), start_dt))
321 end_dt = start_dt.replace(hour=23, minute=59, second=59)
323 work_limits.append((end_dt, end_dt.replace(hour=23, minute=59, second=59)))
324 assert start_dt.date() == end_dt.date(), 'get_working_intervals_of_day is restricted to one day'
327 work_dt = start_dt.replace(hour=0, minute=0, second=0)
329 # no calendar: try to use the default_interval, then return directly
332 working_interval = (start_dt.replace(hour=default_interval[0], minute=0, second=0), start_dt.replace(hour=default_interval[1], minute=0, second=0))
333 intervals = self.interval_remove_leaves(working_interval, work_limits)
336 working_intervals = []
337 for calendar_working_day in self.get_attendances_for_weekdays(cr, uid, id, [start_dt.weekday()], context):
339 work_dt.replace(hour=int(calendar_working_day.hour_from)),
340 work_dt.replace(hour=int(calendar_working_day.hour_to))
342 working_intervals += self.interval_remove_leaves(working_interval, work_limits)
344 # find leave intervals
345 if leaves is None and compute_leaves:
346 leaves = self.get_leave_intervals(cr, uid, id, resource_id=resource_id, context=None)
348 # filter according to leaves
349 for interval in working_intervals:
350 work_intervals = self.interval_remove_leaves(interval, leaves)
351 intervals += work_intervals
355 def get_working_hours_of_date(self, cr, uid, id, start_dt=None, end_dt=None,
356 leaves=None, compute_leaves=False, resource_id=None,
357 default_interval=None, context=None):
358 """ Get the working hours of the day based on calendar. This method uses
359 get_working_intervals_of_day to have the work intervals of the day. It
360 then calculates the number of hours contained in those intervals. """
361 res = datetime.timedelta()
362 intervals = self.get_working_intervals_of_day(
364 start_dt, end_dt, leaves,
365 compute_leaves, resource_id,
366 default_interval, context)
367 for interval in intervals:
368 res += interval[1] - interval[0]
369 return seconds(res) / 3600.0
371 def get_working_hours(self, cr, uid, id, start_dt, end_dt, compute_leaves=False,
372 resource_id=None, default_interval=None, context=None):
374 for day in rrule.rrule(rrule.DAILY, dtstart=start_dt,
375 until=(end_dt + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0),
376 byweekday=self.get_weekdays(cr, uid, id, context=context)):
377 day_start_dt = day.replace(hour=0, minute=0, second=0)
378 if start_dt and day.date() == start_dt.date():
379 day_start_dt = start_dt
380 day_end_dt = day.replace(hour=23, minute=59, second=59)
381 if end_dt and day.date() == end_dt.date():
383 hours += self.get_working_hours_of_date(
384 cr, uid, id, start_dt=day_start_dt, end_dt=day_end_dt,
385 compute_leaves=compute_leaves, resource_id=resource_id,
386 default_interval=default_interval,
390 # --------------------------------------------------
392 # --------------------------------------------------
394 def _schedule_hours(self, cr, uid, id, hours, day_dt=None,
395 compute_leaves=False, resource_id=None,
396 default_interval=None, context=None):
397 """ Schedule hours of work, using a calendar and an optional resource to
398 compute working and leave days. This method can be used backwards, i.e.
399 scheduling days before a deadline.
401 :param int hours: number of hours to schedule. Use a negative number to
402 compute a backwards scheduling.
403 :param datetime day_dt: reference date to compute working days. If days is
404 > 0 date is the starting date. If days is < 0
405 date is the ending date.
406 :param boolean compute_leaves: if set, compute the leaves based on calendar
407 and resource. Otherwise no leaves are taken
409 :param int resource_id: the id of the resource to take into account when
410 computing the leaves. If not set, only general
411 leaves are computed. If set, generic and
412 specific leaves are computed.
413 :param tuple default_interval: if no id, try to return a default working
414 day using default_interval[0] as beginning
415 hour, and default_interval[1] as ending hour.
416 Example: default_interval = (8, 16).
417 Otherwise, a void list of working intervals
418 is returned when id is None.
420 :return tuple (datetime, intervals): datetime is the beginning/ending date
421 of the schedulign; intervals are the
422 working intervals of the scheduling.
424 Note: Why not using rrule.rrule ? Because rrule does not seem to allow
425 getting back in time.
428 day_dt = datetime.datetime.now()
429 backwards = (hours < 0)
432 remaining_hours = hours * 1.0
434 current_datetime = day_dt
436 call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, default_interval=default_interval, context=context)
438 while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000:
440 call_args['end_dt'] = current_datetime
442 call_args['start_dt'] = current_datetime
444 working_intervals = self.get_working_intervals_of_day(cr, uid, id, **call_args)
446 if id is None and not working_intervals: # no calendar -> consider working 8 hours
447 remaining_hours -= 8.0
448 elif working_intervals:
450 working_intervals.reverse()
451 new_working_intervals = self.interval_schedule_hours(working_intervals, remaining_hours, not backwards)
453 new_working_intervals.reverse()
455 res = datetime.timedelta()
456 for interval in working_intervals:
457 res += interval[1] - interval[0]
458 remaining_hours -= (seconds(res) / 3600.0)
460 intervals = new_working_intervals + intervals
462 intervals = intervals + new_working_intervals
465 current_datetime = datetime.datetime.combine(self.get_previous_day(cr, uid, id, current_datetime, context), datetime.time(23, 59, 59))
467 current_datetime = datetime.datetime.combine(self.get_next_day(cr, uid, id, current_datetime, context), datetime.time())
468 # avoid infinite loops
473 def schedule_hours_get_date(self, cr, uid, id, hours, day_dt=None,
474 compute_leaves=False, resource_id=None,
475 default_interval=None, context=None):
476 """ Wrapper on _schedule_hours: return the beginning/ending datetime of
477 an hours scheduling. """
478 res = self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, default_interval, context)
479 return res and res[0][0] or False
481 def schedule_hours(self, cr, uid, id, hours, day_dt=None,
482 compute_leaves=False, resource_id=None,
483 default_interval=None, context=None):
484 """ Wrapper on _schedule_hours: return the working intervals of an hours
486 return self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, default_interval, context)
488 # --------------------------------------------------
490 # --------------------------------------------------
492 def _schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False,
493 resource_id=None, default_interval=None, context=None):
494 """Schedule days of work, using a calendar and an optional resource to
495 compute working and leave days. This method can be used backwards, i.e.
496 scheduling days before a deadline.
498 :param int days: number of days to schedule. Use a negative number to
499 compute a backwards scheduling.
500 :param date day_date: reference date to compute working days. If days is > 0
501 date is the starting date. If days is < 0 date is the
503 :param boolean compute_leaves: if set, compute the leaves based on calendar
504 and resource. Otherwise no leaves are taken
506 :param int resource_id: the id of the resource to take into account when
507 computing the leaves. If not set, only general
508 leaves are computed. If set, generic and
509 specific leaves are computed.
510 :param tuple default_interval: if no id, try to return a default working
511 day using default_interval[0] as beginning
512 hour, and default_interval[1] as ending hour.
513 Example: default_interval = (8, 16).
514 Otherwise, a void list of working intervals
515 is returned when id is None.
517 :return tuple (datetime, intervals): datetime is the beginning/ending date
518 of the schedulign; intervals are the
519 working intervals of the scheduling.
521 Implementation note: rrule.rrule is not used because rrule it des not seem
522 to allow getting back in time.
525 day_date = datetime.datetime.now()
526 backwards = (days < 0)
532 current_datetime = day_date.replace(hour=23, minute=59, second=59)
534 current_datetime = day_date.replace(hour=0, minute=0, second=0)
536 while planned_days < days and iterations < 1000:
537 working_intervals = self.get_working_intervals_of_day(
538 cr, uid, id, current_datetime,
539 compute_leaves=compute_leaves, resource_id=resource_id,
540 default_interval=default_interval,
542 if id is None or working_intervals: # no calendar -> no working hours, but day is considered as worked
544 intervals += working_intervals
547 current_datetime = self.get_previous_day(cr, uid, id, current_datetime, context)
549 current_datetime = self.get_next_day(cr, uid, id, current_datetime, context)
550 # avoid infinite loops
555 def schedule_days_get_date(self, cr, uid, id, days, day_date=None, compute_leaves=False,
556 resource_id=None, default_interval=None, context=None):
557 """ Wrapper on _schedule_days: return the beginning/ending datetime of
558 a days scheduling. """
559 res = self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, default_interval, context)
560 return res and res[-1][1] or False
562 def schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False,
563 resource_id=None, default_interval=None, context=None):
564 """ Wrapper on _schedule_days: return the working intervals of a days
566 return self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, default_interval, context)
568 # --------------------------------------------------
569 # Compatibility / to clean / to remove
570 # --------------------------------------------------
572 def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
573 """ Used in hr_payroll/hr_payroll.py
575 :deprecated: OpenERP saas-3. Use get_working_hours_of_date instead. Note:
576 since saas-3, take hour/minutes into account, not just the whole day."""
577 if isinstance(day, datetime.datetime):
578 day = day.replace(hour=0, minute=0)
579 return self.get_working_hours_of_date(cr, uid, resource_calendar_id.id, start_dt=day, context=None)
581 def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
582 """ Schedule hours backwards. Used in mrp_operations/mrp_operations.py.
584 :deprecated: OpenERP saas-3. Use schedule_hours instead. Note: since
585 saas-3, counts leave hours instead of all-day leaves."""
586 return self.schedule_hours(
587 cr, uid, id, hours * -1.0,
588 day_dt=dt_from.replace(minute=0, second=0),
589 compute_leaves=True, resource_id=resource,
590 default_interval=(8, 16)
593 def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
594 """ Used in mrp_operations/mrp_operations.py (default parameters) and in
597 :deprecated: OpenERP saas-3. Use schedule_hours instead. Note:
598 Byday was not used. Since saas-3, counts Leave hours instead of all-day leaves."""
600 for dt_str, hours, calendar_id in date_and_hours_by_cal:
601 result = self.schedule_hours(
602 cr, uid, calendar_id, hours,
603 day_dt=datetime.datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S').replace(minute=0, second=0),
604 compute_leaves=True, resource_id=resource,
605 default_interval=(8, 16)
607 res[(dt_str, hours, calendar_id)] = result
610 def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
611 """ Unifier of interval_get_multi. Used in: mrp_operations/mrp_operations.py,
612 crm/crm_lead.py (res given).
614 :deprecated: OpenERP saas-3. Use get_working_hours instead."""
615 res = self.interval_get_multi(
616 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)]
619 def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
622 :deprecated: OpenERP saas-3. Use get_working_hours instead."""
623 return self._interval_hours_get(cr, uid, id, dt_from, dt_to, resource_id=resource)
625 def _interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource_id=False, timezone_from_uid=None, exclude_leaves=True, context=None):
626 """ Computes working hours between two dates, taking always same hour/minuts.
628 :deprecated: OpenERP saas-3. Use get_working_hours instead. Note: since saas-3,
629 now resets hour/minuts. Now counts leave hours instead of all-day leaves."""
630 return self.get_working_hours(
631 cr, uid, id, dt_from, dt_to,
632 compute_leaves=(not exclude_leaves), resource_id=resource_id,
633 default_interval=(8, 16), context=context)
636 class resource_calendar_attendance(osv.osv):
637 _name = "resource.calendar.attendance"
638 _description = "Work Detail"
641 'name' : fields.char("Name", required=True),
642 'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of Week', required=True, select=True),
643 'date_from' : fields.date('Starting Date'),
644 'hour_from' : fields.float('Work from', required=True, help="Start and End time of working.", select=True),
645 'hour_to' : fields.float("Work to", required=True),
646 'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
649 _order = 'dayofweek, hour_from'
655 def hours_time_string(hours):
656 """ convert a number of hours (float) into a string with format '%H:%M' """
657 minutes = int(round(hours * 60))
658 return "%02d:%02d" % divmod(minutes, 60)
660 class resource_resource(osv.osv):
661 _name = "resource.resource"
662 _description = "Resource Detail"
664 'name': fields.char("Name", required=True),
665 'code': fields.char('Code', size=16, copy=False),
666 '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."),
667 'company_id' : fields.many2one('res.company', 'Company'),
668 'resource_type': fields.selection([('user','Human'),('material','Material')], 'Resource Type', required=True),
669 'user_id' : fields.many2one('res.users', 'User', help='Related user name for the resource to manage its access.'),
670 '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%."),
671 'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
674 'resource_type' : 'user',
675 'time_efficiency' : 1,
677 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
681 def copy(self, cr, uid, id, default=None, context=None):
684 if not default.get('name', False):
685 default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
686 return super(resource_resource, self).copy(cr, uid, id, default, context)
688 def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
690 Return a list of Resource Class objects for the resources allocated to the phase.
692 NOTE: Used in project/project.py
695 user_pool = self.pool.get('res.users')
696 for user in user_pool.browse(cr, uid, user_ids, context=context):
697 resource_objs[user.id] = {
703 resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
705 for resource in self.browse(cr, uid, resource_ids, context=context):
706 resource_objs[user.id]['efficiency'] = resource.time_efficiency
707 resource_cal = resource.calendar_id.id
709 leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
710 resource_objs[user.id]['vacation'] += list(leaves)
713 def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
715 Compute the vacation from the working calendar of the resource.
717 @param calendar_id : working calendar of the project
718 @param resource_id : resource working on phase/task
719 @param resource_calendar : working calendar of the resource
721 NOTE: used in project/project.py, and in generate_resources
723 resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
726 leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
727 ('calendar_id', '=', resource_calendar),
728 ('resource_id', '=', resource_id)
731 leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
732 ('resource_id', '=', False)
734 leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
735 for i in range(len(leaves)):
736 dt_start = datetime.datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
737 dt_end = datetime.datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
738 no = dt_end - dt_start
739 [leave_list.append((dt_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
743 def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
745 Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
746 @param calendar_id : working calendar of the project
748 NOTE: used in project/project.py
751 # Calendar is not specified: working days: 24/7
752 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'),
753 ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')]
754 resource_attendance_pool = self.pool.get('resource.calendar.attendance')
755 time_range = "8:00-8:00"
757 week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
762 week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context)
763 weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context)
764 # Convert time formats into appropriate format required
765 # and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')]
769 if week_days.get(week['dayofweek'],False):
770 day = week_days[week['dayofweek']]
771 wk_days[week['dayofweek']] = week_days[week['dayofweek']]
773 raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!'))
774 hour_from_str = hours_time_string(week['hour_from'])
775 hour_to_str = hours_time_string(week['hour_to'])
776 res_str = hour_from_str + '-' + hour_to_str
777 wktime_list.append((day, res_str))
778 # Convert into format like [('mon', '8:00-12:00', '13:00-18:00')]
779 for item in wktime_list:
780 if wk_time.has_key(item[0]):
781 wk_time[item[0]].append(item[1])
783 wk_time[item[0]] = [item[0]]
784 wk_time[item[0]].append(item[1])
785 for k,v in wk_time.items():
786 wktime_cal.append(tuple(v))
787 # Add for the non-working days like: [('sat, sun', '8:00-8:00')]
788 for k, v in wk_days.items():
789 if week_days.has_key(k):
791 for v in week_days.itervalues():
792 non_working += v + ','
794 wktime_cal.append((non_working[:-1], time_range))
798 class resource_calendar_leaves(osv.osv):
799 _name = "resource.calendar.leaves"
800 _description = "Leave Detail"
802 'name' : fields.char("Name"),
803 'company_id' : fields.related('calendar_id','company_id',type='many2one',relation='res.company',string="Company", store=True, readonly=True),
804 'calendar_id' : fields.many2one("resource.calendar", "Working Time"),
805 'date_from' : fields.datetime('Start Date', required=True),
806 'date_to' : fields.datetime('End Date', required=True),
807 '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"),
810 def check_dates(self, cr, uid, ids, context=None):
811 for leave in self.browse(cr, uid, ids, context=context):
812 if leave.date_from and leave.date_to and leave.date_from > leave.date_to:
817 (check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
820 def onchange_resource(self, cr, uid, ids, resource, context=None):
823 resource_pool = self.pool.get('resource.resource')
824 result['calendar_id'] = resource_pool.browse(cr, uid, resource, context=context).calendar_id.id
825 return {'value': result}
826 return {'value': {'calendar_id': []}}
829 assert isinstance(td, datetime.timedelta)
831 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.**6
833 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: