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 ##############################################################################
22 from datetime import datetime, timedelta
25 from osv import fields, osv
26 from tools.translate import _
28 from itertools import groupby
29 from operator import itemgetter
32 class resource_calendar(osv.osv):
33 _name = "resource.calendar"
34 _description = "Resource Calendar"
36 'name' : fields.char("Name", size=64, required=True),
37 'company_id' : fields.many2one('res.company', 'Company', required=False),
38 'attendance_ids' : fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
39 'manager' : fields.many2one('res.users', 'Workgroup Manager'),
42 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
45 def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
46 """Calculates the Working Total Hours based on Resource Calendar and
47 given working day (datetime object).
49 @param resource_calendar_id: resource.calendar browse record
50 @param day: datetime object
52 @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
55 for working_day in resource_calendar_id.attendance_ids:
56 if (int(working_day.dayofweek) + 1) == day.isoweekday():
57 res += working_day.hour_to - working_day.hour_from
60 def _get_leaves(self, cr, uid, id, resource):
61 """Private Method to Calculate resource Leaves days
63 @param id: resource calendar id
64 @param resource: resource id for which leaves will ew calculated
66 @return : returns the list of dates, where resource on leave in
67 resource.calendar.leaves object (e.g.['%Y-%m-%d', '%Y-%m-%d'])
69 resource_cal_leaves = self.pool.get('resource.calendar.leaves')
71 resource_leave_ids = resource_cal_leaves.search(cr, uid, [('calendar_id','=',id), '|', ('resource_id','=',False), ('resource_id','=',resource)])
72 #res_leaves = resource_cal_leaves.read(cr, uid, resource_leave_ids, ['date_from', 'date_to'])
73 res_leaves = resource_cal_leaves.browse(cr, uid, resource_leave_ids)
75 for leave in res_leaves:
76 dtf = datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S')
77 dtt = datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S')
79 [dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
84 def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
86 Calculates the working Schedule from supplied from date to till hours
87 will be satisfied based or resource calendar id. If resource is also
88 given then it will consider the resource leave also and than will
89 calculates resource working schedule
91 @param dt_from: datetime object, start of working scheduled
92 @param hours: float, total number working hours needed scheduled from
94 @param resource : Optional Resource id, if supplied than resource leaves
95 will also taken into consideration for calculating working
97 @return : List datetime object of working schedule based on supplies
102 return [(dt_from - timedelta(hours=td), dt_from)]
103 dt_leave = self._get_leaves(cr, uid, id, resource)
108 current_hour = dt_from.hour
109 while (todo>0) and maxrecur:
110 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))
111 for (hour_from,hour_to) in cr.fetchall():
113 if (hour_from<current_hour) and (todo>0):
114 m = min(hour_to, current_hour)
115 if (m-hour_from)>todo:
117 dt_check = dt_from.strftime('%Y-%m-%d')
118 for leave in dt_leave:
119 if dt_check == leave:
120 dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
125 d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60))
126 d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
127 result.append((d1, d2))
128 current_hour = hour_from
129 todo -= (m-hour_from)
130 dt_from -= timedelta(days=1)
136 # def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
137 def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
139 lst.sort(key=itemgetter(key))
140 grouped = groupby(lst, itemgetter(key))
141 return dict([(k, [v for v in itr]) for k, itr in grouped])
144 cr.execute("select calendar_id, dayofweek, hour_from, hour_to from resource_calendar_attendance order by hour_from")
145 hour_res = cr.dictfetchall()
146 hours_by_cal = group(hour_res, 'calendar_id')
150 for d, hours, id in date_and_hours_by_cal:
151 dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S')
154 results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))]
157 dt_leave = self._get_leaves(cr, uid, id, resource)
161 current_hour = dt_from.hour
162 while (todo>0) and maxrecur:
163 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())]:
165 if (hour_to>current_hour) and (todo>0):
166 m = max(hour_from, current_hour)
169 dt_check = dt_from.strftime('%Y-%m-%d')
170 for leave in dt_leave:
171 if dt_check == leave:
172 dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
177 d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
178 d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
179 result.append((d1, d2))
180 current_hour = hour_to
181 todo -= (hour_to - m)
182 dt_from += timedelta(days=1)
185 results[(d, hours, id)] = result
188 def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
189 """Calculates Resource Working Internal Timing Based on Resource Calendar.
191 @param dt_from: start resource schedule calculation.
192 @param hours : total number of working hours to be scheduled.
193 @param resource: optional resource id, If supplied it will take care of
194 resource leave while scheduling.
195 @param byday: boolean flag bit enforce day wise scheduling
197 @return : list of scheduled working timing based on resource calendar.
199 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)]
202 def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
203 """ Calculates the Total Working hours based on given start_date to
204 end_date, If resource id is supplied that it will consider the source
205 leaves also in calculating the hours.
207 @param dt_from : date start to calculate hours
208 @param dt_end : date end to calculate hours
209 @param resource: optional resource id, If given resource leave will be
212 @return : Total number of working hours based dt_from and dt_end and
213 resource if supplied.
217 dt_leave = self._get_leaves(cr, uid, id, resource)
220 current_hour = dt_from.hour
222 while (dt_from <= dt_to):
223 cr.execute("select hour_from,hour_to from resource_calendar_attendance where dayofweek='%s' and calendar_id=%s order by hour_from", (dt_from.weekday(),id))
225 for (hour_from,hour_to) in der:
226 if hours != 0.0:#For first time of the loop only,hours will be 0
227 current_hour = hour_from
229 if (hour_to>=current_hour):
230 dt_check = dt_from.strftime('%Y-%m-%d')
231 for leave in dt_leave:
232 if dt_check == leave:
233 dt_check = datetime.strptime(dt_check, "%Y-%m-%d") + timedelta(days=1)
240 d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
242 if hours != 0.0:#For first time of the loop only,hours will be 0
243 d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(current_hour)), int((current_hour%1) * 60))
245 if dt_from.day == dt_to.day:
246 if hour_from <= dt_to.hour <= hour_to:
249 hours += (d2-d1).seconds
250 dt_from = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(current_hour)), int((current_hour%1) * 60)) + timedelta(days=1)
257 class resource_calendar_attendance(osv.osv):
258 _name = "resource.calendar.attendance"
259 _description = "Work Detail"
262 'name' : fields.char("Name", size=64, required=True),
263 'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of Week', required=True, select=True),
264 'date_from' : fields.date('Starting Date'),
265 'hour_from' : fields.float('Work from', size=8, required=True, help="Working time will start from", select=True),
266 'hour_to' : fields.float("Work to", size=8, required=True, help="Working time will end at"),
267 'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
270 _order = 'dayofweek, hour_from'
275 resource_calendar_attendance()
277 def convert_timeformat(time_string):
278 split_list = str(time_string).split('.')
279 hour_part = split_list[0]
280 mins_part = split_list[1]
281 round_mins = int(round(float(mins_part) * 60,-2))
282 converted_string = hour_part + ':' + str(round_mins)[0:2]
283 return converted_string
285 class resource_resource(osv.osv):
286 _name = "resource.resource"
287 _description = "Resource Detail"
289 'name' : fields.char("Name", size=64, required=True),
290 'code': fields.char('Code', size=16),
291 '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."),
292 'company_id' : fields.many2one('res.company', 'Company'),
293 'resource_type': fields.selection([('user','Human'),('material','Material')], 'Resource Type', required=True),
294 'user_id' : fields.many2one('res.users', 'User', help='Related user name for the resource to manage its access.'),
295 '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 efficency of 200%, then his load will only be 50%."),
296 'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
299 'resource_type' : 'user',
300 'time_efficiency' : 1,
302 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
306 def copy(self, cr, uid, id, default=None, context=None):
309 if not default.get('name', False):
310 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
311 return super(resource_resource, self).copy(cr, uid, id, default, context)
313 def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
315 Return a list of Resource Class objects for the resources allocated to the phase.
318 user_pool = self.pool.get('res.users')
319 for user in user_pool.browse(cr, uid, user_ids, context=context):
320 resource_objs[user.id] = {
326 resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
328 for resource in self.browse(cr, uid, resource_ids, context=context):
329 resource_objs[user.id]['efficiency'] = resource.time_efficiency
330 resource_cal = resource.calendar_id.id
332 leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
333 resource_objs[user.id]['vacation'] += list(leaves)
336 def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
338 Compute the vacation from the working calendar of the resource.
340 @param calendar_id : working calendar of the project
341 @param resource_id : resource working on phase/task
342 @param resource_calendar : working calendar of the resource
344 resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
347 leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
348 ('calendar_id', '=', resource_calendar),
349 ('resource_id', '=', resource_id)
352 leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
353 ('resource_id', '=', False)
355 leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
356 for i in range(len(leaves)):
357 dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
358 dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
359 no = dt_end - dt_start
360 [leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
364 def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
366 Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
367 @param calendar_id : working calendar of the project
370 # Calendar is not specified: working days: 24/7
371 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'),
372 ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')]
373 resource_attendance_pool = self.pool.get('resource.calendar.attendance')
374 time_range = "8:00-8:00"
376 week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
381 week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context)
382 weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context)
383 # Convert time formats into appropriate format required
384 # and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')]
388 if week_days.get(week['dayofweek'],False):
389 day = week_days[week['dayofweek']]
390 wk_days[week['dayofweek']] = week_days[week['dayofweek']]
392 raise osv.except_osv(_('Configuration Error!'),_('Make sure the Working time has been configured with proper week days!'))
393 hour_from_str = convert_timeformat(week['hour_from'])
394 hour_to_str = convert_timeformat(week['hour_to'])
395 res_str = hour_from_str + '-' + hour_to_str
396 wktime_list.append((day, res_str))
397 # Convert into format like [('mon', '8:00-12:00', '13:00-18:00')]
398 for item in wktime_list:
399 if wk_time.has_key(item[0]):
400 wk_time[item[0]].append(item[1])
402 wk_time[item[0]] = [item[0]]
403 wk_time[item[0]].append(item[1])
404 for k,v in wk_time.items():
405 wktime_cal.append(tuple(v))
406 # Add for the non-working days like: [('sat, sun', '8:00-8:00')]
407 for k, v in wk_days.items():
408 if week_days.has_key(k):
410 for v in week_days.itervalues():
411 non_working += v + ','
413 wktime_cal.append((non_working[:-1], time_range))
418 class resource_calendar_leaves(osv.osv):
419 _name = "resource.calendar.leaves"
420 _description = "Leave Detail"
422 'name' : fields.char("Name", size=64),
423 'company_id' : fields.related('calendar_id','company_id',type='many2one',relation='res.company',string="Company", store=True, readonly=True),
424 'calendar_id' : fields.many2one("resource.calendar", "Working Time"),
425 'date_from' : fields.datetime('Start Date', required=True),
426 'date_to' : fields.datetime('End Date', required=True),
427 '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"),
430 def check_dates(self, cr, uid, ids, context=None):
431 leave = self.read(cr, uid, ids[0], ['date_from', 'date_to'])
432 if leave['date_from'] and leave['date_to']:
433 if leave['date_from'] > leave['date_to']:
438 (check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
441 def onchange_resource(self,cr, uid, ids, resource, context=None):
444 resource_pool = self.pool.get('resource.resource')
445 result['calendar_id'] = resource_pool.browse(cr, uid, resource, context=context).calendar_id.id
446 return {'value': result}
447 return {'value': {'calendar_id': []}}
449 resource_calendar_leaves()
451 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: