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", size=64, required=True),
48 'company_id': fields.many2one('res.company', 'Company', required=False),
49 'attendance_ids': fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
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 intervals.append((start_dt.replace(hour=default_interval[0]), start_dt.replace(hour=default_interval[1])))
335 working_intervals = []
336 for calendar_working_day in self.get_attendances_for_weekdays(cr, uid, id, [start_dt.weekday()], context):
338 work_dt.replace(hour=int(calendar_working_day.hour_from)),
339 work_dt.replace(hour=int(calendar_working_day.hour_to))
341 working_intervals += self.interval_remove_leaves(working_interval, work_limits)
343 # find leave intervals
344 if leaves is None and compute_leaves:
345 leaves = self.get_leave_intervals(cr, uid, id, resource_id=resource_id, context=None)
347 # filter according to leaves
348 for interval in working_intervals:
349 work_intervals = self.interval_remove_leaves(interval, leaves)
350 intervals += work_intervals
354 def get_working_hours_of_date(self, cr, uid, id, start_dt=None, end_dt=None,
355 leaves=None, compute_leaves=False, resource_id=None,
356 default_interval=None, context=None):
357 """ Get the working hours of the day based on calendar. This method uses
358 get_working_intervals_of_day to have the work intervals of the day. It
359 then calculates the number of hours contained in those intervals. """
360 res = datetime.timedelta()
361 intervals = self.get_working_intervals_of_day(
363 start_dt, end_dt, leaves,
364 compute_leaves, resource_id,
365 default_interval, context)
366 for interval in intervals:
367 res += interval[1] - interval[0]
368 return seconds(res) / 3600.0
370 def get_working_hours(self, cr, uid, id, start_dt, end_dt, compute_leaves=False,
371 resource_id=None, default_interval=None, context=None):
373 for day in rrule.rrule(rrule.DAILY, dtstart=start_dt,
374 until=end_dt + datetime.timedelta(days=1),
375 byweekday=self.get_weekdays(cr, uid, id, context=context)):
376 hours += self.get_working_hours_of_date(
377 cr, uid, id, start_dt=day,
378 compute_leaves=compute_leaves, resource_id=resource_id,
379 default_interval=default_interval,
383 # --------------------------------------------------
385 # --------------------------------------------------
387 def _schedule_hours(self, cr, uid, id, hours, day_dt=None,
388 compute_leaves=False, resource_id=None,
389 default_interval=None, context=None):
390 """ Schedule hours of work, using a calendar and an optional resource to
391 compute working and leave days. This method can be used backwards, i.e.
392 scheduling days before a deadline.
394 :param int hours: number of hours to schedule. Use a negative number to
395 compute a backwards scheduling.
396 :param datetime day_dt: reference date to compute working days. If days is
397 > 0 date is the starting date. If days is < 0
398 date is the ending date.
399 :param boolean compute_leaves: if set, compute the leaves based on calendar
400 and resource. Otherwise no leaves are taken
402 :param int resource_id: the id of the resource to take into account when
403 computing the leaves. If not set, only general
404 leaves are computed. If set, generic and
405 specific leaves are computed.
406 :param tuple default_interval: if no id, try to return a default working
407 day using default_interval[0] as beginning
408 hour, and default_interval[1] as ending hour.
409 Example: default_interval = (8, 16).
410 Otherwise, a void list of working intervals
411 is returned when id is None.
413 :return tuple (datetime, intervals): datetime is the beginning/ending date
414 of the schedulign; intervals are the
415 working intervals of the scheduling.
417 Note: Why not using rrule.rrule ? Because rrule does not seem to allow
418 getting back in time.
421 day_dt = datetime.datetime.now()
422 backwards = (hours < 0)
425 remaining_hours = hours * 1.0
427 current_datetime = day_dt
429 call_args = dict(compute_leaves=compute_leaves, resource_id=resource_id, default_interval=default_interval, context=context)
431 while float_compare(remaining_hours, 0.0, precision_digits=2) in (1, 0) and iterations < 1000:
433 call_args['end_dt'] = current_datetime
435 call_args['start_dt'] = current_datetime
437 working_intervals = self.get_working_intervals_of_day(cr, uid, id, **call_args)
439 if id is None and not working_intervals: # no calendar -> consider working 8 hours
440 remaining_hours -= 8.0
441 elif working_intervals:
443 working_intervals.reverse()
444 new_working_intervals = self.interval_schedule_hours(working_intervals, remaining_hours, not backwards)
446 new_working_intervals.reverse()
448 res = datetime.timedelta()
449 for interval in working_intervals:
450 res += interval[1] - interval[0]
451 remaining_hours -= (seconds(res) / 3600.0)
453 intervals = new_working_intervals + intervals
455 intervals = intervals + new_working_intervals
458 current_datetime = datetime.datetime.combine(self.get_previous_day(cr, uid, id, current_datetime, context), datetime.time(23, 59, 59))
460 current_datetime = datetime.datetime.combine(self.get_next_day(cr, uid, id, current_datetime, context), datetime.time())
461 # avoid infinite loops
466 def schedule_hours_get_date(self, cr, uid, id, hours, day_dt=None,
467 compute_leaves=False, resource_id=None,
468 default_interval=None, context=None):
469 """ Wrapper on _schedule_hours: return the beginning/ending datetime of
470 an hours scheduling. """
471 res = self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, default_interval, context)
472 return res and res[0][0] or False
474 def schedule_hours(self, cr, uid, id, hours, day_dt=None,
475 compute_leaves=False, resource_id=None,
476 default_interval=None, context=None):
477 """ Wrapper on _schedule_hours: return the working intervals of an hours
479 return self._schedule_hours(cr, uid, id, hours, day_dt, compute_leaves, resource_id, default_interval, context)
481 # --------------------------------------------------
483 # --------------------------------------------------
485 def _schedule_days(self, cr, uid, id, days, day_date=None, compute_leaves=False,
486 resource_id=None, default_interval=None, context=None):
487 """Schedule days of work, using a calendar and an optional resource to
488 compute working and leave days. This method can be used backwards, i.e.
489 scheduling days before a deadline.
491 :param int days: number of days to schedule. Use a negative number to
492 compute a backwards scheduling.
493 :param date day_date: reference date to compute working days. If days is > 0
494 date is the starting date. If days is < 0 date is the
496 :param boolean compute_leaves: if set, compute the leaves based on calendar
497 and resource. Otherwise no leaves are taken
499 :param int resource_id: the id of the resource to take into account when
500 computing the leaves. If not set, only general
501 leaves are computed. If set, generic and
502 specific leaves are computed.
503 :param tuple default_interval: if no id, try to return a default working
504 day using default_interval[0] as beginning
505 hour, and default_interval[1] as ending hour.
506 Example: default_interval = (8, 16).
507 Otherwise, a void list of working intervals
508 is returned when id is None.
510 :return tuple (datetime, intervals): datetime is the beginning/ending date
511 of the schedulign; intervals are the
512 working intervals of the scheduling.
514 Implementation note: rrule.rrule is not used because rrule it des not seem
515 to allow getting back in time.
518 day_date = datetime.datetime.now()
519 backwards = (days < 0)
525 current_datetime = day_date.replace(hour=23, minute=59, second=59)
527 current_datetime = day_date.replace(hour=0, minute=0, second=0)
529 while planned_days < days and iterations < 1000:
530 working_intervals = self.get_working_intervals_of_day(
531 cr, uid, id, current_datetime,
532 compute_leaves=compute_leaves, resource_id=resource_id,
533 default_interval=default_interval,
535 if id is None or working_intervals: # no calendar -> no working hours, but day is considered as worked
537 intervals += working_intervals
540 current_datetime = self.get_previous_day(cr, uid, id, current_datetime, context)
542 current_datetime = self.get_next_day(cr, uid, id, current_datetime, context)
543 # avoid infinite loops
548 def schedule_days_get_date(self, cr, uid, id, days, day_date=None, compute_leaves=False,
549 resource_id=None, default_interval=None, context=None):
550 """ Wrapper on _schedule_days: return the beginning/ending datetime of
551 a days scheduling. """
552 res = self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, default_interval, context)
553 return res and res[-1][1] or False
555 def schedule_days(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 working intervals of a days
559 return self._schedule_days(cr, uid, id, days, day_date, compute_leaves, resource_id, default_interval, context)
561 # --------------------------------------------------
562 # Compatibility / to clean / to remove
563 # --------------------------------------------------
565 def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
566 """ Used in hr_payroll/hr_payroll.py
568 :deprecated: OpenERP saas-3. Use get_working_hours_of_date instead. Note:
569 since saas-3, take hour/minutes into account, not just the whole day."""
570 if isinstance(day, datetime.datetime):
571 day = day.replace(hour=0, minute=0)
572 return self.get_working_hours_of_date(cr, uid, resource_calendar_id.id, start_dt=day, context=None)
574 def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
575 """ Schedule hours backwards. Used in mrp_operations/mrp_operations.py.
577 :deprecated: OpenERP saas-3. Use schedule_hours instead. Note: since
578 saas-3, counts leave hours instead of all-day leaves."""
579 return self.schedule_hours(
580 cr, uid, id, hours * -1.0,
581 day_dt=dt_from.replace(minute=0, second=0),
582 compute_leaves=True, resource_id=resource,
583 default_interval=(8, 16)
586 def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
587 """ Used in mrp_operations/mrp_operations.py (default parameters) and in
590 :deprecated: OpenERP saas-3. Use schedule_hours instead. Note:
591 Byday was not used. Since saas-3, counts Leave hours instead of all-day leaves."""
593 for dt_str, hours, calendar_id in date_and_hours_by_cal:
594 result = self.schedule_hours(
595 cr, uid, calendar_id, hours,
596 day_dt=datetime.datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S').replace(minute=0, second=0),
597 compute_leaves=True, resource_id=resource,
598 default_interval=(8, 16)
600 res[(dt_str, hours, calendar_id)] = result
603 def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
604 """ Unifier of interval_get_multi. Used in: mrp_operations/mrp_operations.py,
605 crm/crm_lead.py (res given).
607 :deprecated: OpenERP saas-3. Use get_working_hours instead."""
608 res = self.interval_get_multi(
609 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)]
612 def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
615 :deprecated: OpenERP saas-3. Use get_working_hours instead."""
616 return self._interval_hours_get(cr, uid, id, dt_from, dt_to, resource_id=resource)
618 def _interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource_id=False, timezone_from_uid=None, exclude_leaves=True, context=None):
619 """ Computes working hours between two dates, taking always same hour/minuts.
621 :deprecated: OpenERP saas-3. Use get_working_hours instead. Note: since saas-3,
622 now resets hour/minuts. Now counts leave hours instead of all-day leaves."""
623 return self.get_working_hours(
624 cr, uid, id, dt_from, dt_to,
625 compute_leaves=(not exclude_leaves), resource_id=resource_id,
626 default_interval=(8, 16), context=context)
629 class resource_calendar_attendance(osv.osv):
630 _name = "resource.calendar.attendance"
631 _description = "Work Detail"
634 'name' : fields.char("Name", size=64, required=True),
635 'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of Week', required=True, select=True),
636 'date_from' : fields.date('Starting Date'),
637 'hour_from' : fields.float('Work from', required=True, help="Start and End time of working.", select=True),
638 'hour_to' : fields.float("Work to", required=True),
639 'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
642 _order = 'dayofweek, hour_from'
648 def hours_time_string(hours):
649 """ convert a number of hours (float) into a string with format '%H:%M' """
650 minutes = int(round(hours * 60))
651 return "%02d:%02d" % divmod(minutes, 60)
653 class resource_resource(osv.osv):
654 _name = "resource.resource"
655 _description = "Resource Detail"
657 'name' : fields.char("Name", size=64, required=True),
658 'code': fields.char('Code', size=16),
659 '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."),
660 'company_id' : fields.many2one('res.company', 'Company'),
661 'resource_type': fields.selection([('user','Human'),('material','Material')], 'Resource Type', required=True),
662 'user_id' : fields.many2one('res.users', 'User', help='Related user name for the resource to manage its access.'),
663 '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%."),
664 'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
667 'resource_type' : 'user',
668 'time_efficiency' : 1,
670 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
674 def copy(self, cr, uid, id, default=None, context=None):
677 if not default.get('name', False):
678 default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
679 return super(resource_resource, self).copy(cr, uid, id, default, context)
681 def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
683 Return a list of Resource Class objects for the resources allocated to the phase.
685 NOTE: Used in project/project.py
688 user_pool = self.pool.get('res.users')
689 for user in user_pool.browse(cr, uid, user_ids, context=context):
690 resource_objs[user.id] = {
696 resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
698 for resource in self.browse(cr, uid, resource_ids, context=context):
699 resource_objs[user.id]['efficiency'] = resource.time_efficiency
700 resource_cal = resource.calendar_id.id
702 leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
703 resource_objs[user.id]['vacation'] += list(leaves)
706 def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
708 Compute the vacation from the working calendar of the resource.
710 @param calendar_id : working calendar of the project
711 @param resource_id : resource working on phase/task
712 @param resource_calendar : working calendar of the resource
714 NOTE: used in project/project.py, and in generate_resources
716 resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
719 leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
720 ('calendar_id', '=', resource_calendar),
721 ('resource_id', '=', resource_id)
724 leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
725 ('resource_id', '=', False)
727 leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
728 for i in range(len(leaves)):
729 dt_start = datetime.datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
730 dt_end = datetime.datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
731 no = dt_end - dt_start
732 [leave_list.append((dt_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
736 def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
738 Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
739 @param calendar_id : working calendar of the project
741 NOTE: used in project/project.py
744 # Calendar is not specified: working days: 24/7
745 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'),
746 ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')]
747 resource_attendance_pool = self.pool.get('resource.calendar.attendance')
748 time_range = "8:00-8:00"
750 week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
755 week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context)
756 weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context)
757 # Convert time formats into appropriate format required
758 # and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')]
762 if week_days.get(week['dayofweek'],False):
763 day = week_days[week['dayofweek']]
764 wk_days[week['dayofweek']] = week_days[week['dayofweek']]
766 raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!'))
767 hour_from_str = hours_time_string(week['hour_from'])
768 hour_to_str = hours_time_string(week['hour_to'])
769 res_str = hour_from_str + '-' + hour_to_str
770 wktime_list.append((day, res_str))
771 # Convert into format like [('mon', '8:00-12:00', '13:00-18:00')]
772 for item in wktime_list:
773 if wk_time.has_key(item[0]):
774 wk_time[item[0]].append(item[1])
776 wk_time[item[0]] = [item[0]]
777 wk_time[item[0]].append(item[1])
778 for k,v in wk_time.items():
779 wktime_cal.append(tuple(v))
780 # Add for the non-working days like: [('sat, sun', '8:00-8:00')]
781 for k, v in wk_days.items():
782 if week_days.has_key(k):
784 for v in week_days.itervalues():
785 non_working += v + ','
787 wktime_cal.append((non_working[:-1], time_range))
791 class resource_calendar_leaves(osv.osv):
792 _name = "resource.calendar.leaves"
793 _description = "Leave Detail"
795 'name' : fields.char("Name", size=64),
796 'company_id' : fields.related('calendar_id','company_id',type='many2one',relation='res.company',string="Company", store=True, readonly=True),
797 'calendar_id' : fields.many2one("resource.calendar", "Working Time"),
798 'date_from' : fields.datetime('Start Date', required=True),
799 'date_to' : fields.datetime('End Date', required=True),
800 '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"),
803 def check_dates(self, cr, uid, ids, context=None):
804 leave = self.read(cr, uid, ids[0], ['date_from', 'date_to'])
805 if leave['date_from'] and leave['date_to']:
806 if leave['date_from'] > leave['date_to']:
811 (check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
814 def onchange_resource(self, cr, uid, ids, resource, context=None):
817 resource_pool = self.pool.get('resource.resource')
818 result['calendar_id'] = resource_pool.browse(cr, uid, resource, context=context).calendar_id.id
819 return {'value': result}
820 return {'value': {'calendar_id': []}}
823 assert isinstance(td, datetime.timedelta)
825 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10.**6
827 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: