Launchpad automatic translations update.
[odoo/odoo.git] / addons / resource / resource.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta
23 import math
24 from faces import *
25 from osv import fields, osv
26 from tools.translate import _
27
28 from itertools import groupby
29 from operator import itemgetter
30
31
32 class resource_calendar(osv.osv):
33     _name = "resource.calendar"
34     _description = "Resource Calendar"
35     _columns = {
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'),
40     }
41     _defaults = {
42         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
43     }
44
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).
48
49         @param resource_calendar_id: resource.calendar browse record
50         @param day: datetime object
51
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
53         """
54         res = 0.0
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
58         return res
59
60     def _get_leaves(self, cr, uid, id, resource):
61         """Private Method to Calculate resource Leaves days
62
63         @param id: resource calendar id
64         @param resource: resource id for which leaves will ew calculated
65
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'])
68         """
69         resource_cal_leaves = self.pool.get('resource.calendar.leaves')
70         dt_leave = []
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)
74
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')
78             no = dtt - dtf
79             [dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
80             dt_leave.sort()
81
82         return dt_leave
83
84     def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
85         """
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
90
91         @param dt_from: datetime object, start of working scheduled
92         @param hours: float, total number working  hours needed scheduled from
93                       start date
94         @param resource : Optional Resource id, if supplied than resource leaves
95                         will also taken into consideration for calculating working
96                         schedule.
97         @return : List datetime object of working schedule based on supplies
98                   params
99         """
100         if not id:
101             td = int(hours)*3
102             return [(dt_from - timedelta(hours=td), dt_from)]
103         dt_leave = self._get_leaves(cr, uid, id, resource)
104         dt_leave.reverse()
105         todo = hours
106         result = []
107         maxrecur = 100
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():
112                 leave_flag  = False
113                 if (hour_from<current_hour) and (todo>0):
114                     m = min(hour_to, current_hour)
115                     if (m-hour_from)>todo:
116                         hour_from = m-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)
121                             leave_flag = True
122                     if leave_flag:
123                         break
124                     else:
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)
131             current_hour = 24
132             maxrecur -= 1
133         result.reverse()
134         return result
135
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):
138         def group(lst, key):
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])
142         # END group
143
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')
147
148         results = {}
149
150         for d, hours, id in date_and_hours_by_cal:
151             dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S')
152             if not id:
153                 td = int(hours)*3
154                 results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))]
155                 continue
156
157             dt_leave = self._get_leaves(cr, uid, id, resource)
158             todo = hours
159             result = []
160             maxrecur = 100
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())]:
164                     leave_flag  = False
165                     if (hour_to>current_hour) and (todo>0):
166                         m = max(hour_from, current_hour)
167                         if (hour_to-m)>todo:
168                             hour_to = m+todo
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)
173                                 leave_flag = True
174                         if leave_flag:
175                             break
176                         else:
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)
183                 current_hour = 0
184                 maxrecur -= 1
185             results[(d, hours, id)] = result
186         return results
187
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.
190
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
196
197         @return :  list of scheduled working timing  based on resource calendar.
198         """
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)]
200         return res
201
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.
206
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
210                          considered.
211
212         @return : Total number of working hours based dt_from and dt_end and
213                   resource if supplied.
214         """
215         if not id:
216             return 0.0
217         dt_leave = self._get_leaves(cr, uid, id, resource)
218         hours = 0.0
219
220         current_hour = dt_from.hour
221
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))
224             der =  cr.fetchall()
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
228                 leave_flag = False
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)
234                             leave_flag = True
235
236                     if leave_flag:
237                         break
238                     else:
239                         d1 = dt_from
240                         d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
241
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))
244
245                         if dt_from.day == dt_to.day:
246                             if hour_from <= dt_to.hour <= hour_to:
247                                 d2 = dt_to
248                         dt_from = d2
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)
251             current_hour = 0.0
252
253         return (hours/3600)
254
255 resource_calendar()
256
257 class resource_calendar_attendance(osv.osv):
258     _name = "resource.calendar.attendance"
259     _description = "Work Detail"
260
261     _columns = {
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', required=True, help="Start and End time of working.", select=True),
266         'hour_to' : fields.float("Work to", required=True),
267         'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
268     }
269
270     _order = 'dayofweek, hour_from'
271
272     _defaults = {
273         'dayofweek' : '0'
274     }
275 resource_calendar_attendance()
276
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
284
285 class resource_resource(osv.osv):
286     _name = "resource.resource"
287     _description = "Resource Detail"
288     _columns = {
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 efficiency of 200%, then his load will only be 50%."),
296         'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
297     }
298     _defaults = {
299         'resource_type' : 'user',
300         'time_efficiency' : 1,
301         'active' : True,
302         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
303     }
304
305
306     def copy(self, cr, uid, id, default=None, context=None):
307         if default is None:
308             default = {}
309         if not default.get('name', False):
310             default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name))
311         return super(resource_resource, self).copy(cr, uid, id, default, context)
312
313     def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
314         """
315         Return a list of  Resource Class objects for the resources allocated to the phase.
316         """
317         resource_objs = {}
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] = {
321                  'name' : user.name,
322                  'vacation': [],
323                  'efficiency': 1.0,
324             }
325
326             resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
327             if resource_ids:
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
331                     if resource_cal:
332                         leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
333                         resource_objs[user.id]['vacation'] += list(leaves)
334         return resource_objs
335
336     def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
337         """
338         Compute the vacation from the working calendar of the resource.
339
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
343         """
344         resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
345         leave_list = []
346         if resource_id:
347             leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
348                                                                        ('calendar_id', '=', resource_calendar),
349                                                                        ('resource_id', '=', resource_id)
350                                                                       ], context=context)
351         else:
352             leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
353                                                                       ('resource_id', '=', False)
354                                                                       ], context=context)
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))]
361             leave_list.sort()
362         return leave_list
363
364     def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
365         """
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
368         """
369         if not calendar_id:
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"
375         non_working = ""
376         week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
377         wk_days = {}
378         wk_time = {}
379         wktime_list = []
380         wktime_cal = []
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')]
385         for week in weeks:
386             res_str = ""
387             day = None
388             if week_days.get(week['dayofweek'],False):
389                 day = week_days[week['dayofweek']]
390                 wk_days[week['dayofweek']] = week_days[week['dayofweek']]
391             else:
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])
401             else:
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):
409                 week_days.pop(k)
410         for v in week_days.itervalues():
411             non_working += v + ','
412         if non_working:
413             wktime_cal.append((non_working[:-1], time_range))
414         return wktime_cal
415
416 resource_resource()
417
418 class resource_calendar_leaves(osv.osv):
419     _name = "resource.calendar.leaves"
420     _description = "Leave Detail"
421     _columns = {
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"),
428     }
429
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']:
434                  return False
435          return True
436
437     _constraints = [
438         (check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
439     ]
440
441     def onchange_resource(self, cr, uid, ids, resource, context=None):
442         result = {}
443         if resource:
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': []}}
448
449 resource_calendar_leaves()
450
451 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: