[fix] desactivated href in header
[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 new import classobj
26 from osv import fields, osv
27 from tools.translate import _
28
29 from itertools import groupby
30 from operator import itemgetter
31
32
33 class resource_calendar(osv.osv):
34     _name = "resource.calendar"
35     _description = "Resource Calendar"
36     _columns = {
37         'name' : fields.char("Name", size=64, required=True),
38         'company_id' : fields.many2one('res.company', 'Company', required=False),
39         'attendance_ids' : fields.one2many('resource.calendar.attendance', 'calendar_id', 'Working Time'),
40         'manager' : fields.many2one('res.users', 'Workgroup manager'),
41     }
42     _defaults = {
43         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.calendar', context=context)
44     }
45
46     def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None):
47         """
48         @param resource_calendar_id: resource.calendar browse record
49         @param day: datetime object
50         @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
51         """
52         res = 0.0
53         for working_day in resource_calendar_id.attendance_ids:
54             if (int(working_day.dayofweek) + 1) == day.isoweekday():
55                 res += working_day.hour_to - working_day.hour_from
56         return res 
57
58     def _get_leaves(self, cr, uid, id, resource):
59         resource_cal_leaves = self.pool.get('resource.calendar.leaves')
60         dt_leave = []
61
62         resource_leave_ids = resource_cal_leaves.search(cr, uid, [('calendar_id','=',id), '|', ('resource_id','=',False), ('resource_id','=',resource)])
63         #res_leaves = resource_cal_leaves.read(cr, uid, resource_leave_ids, ['date_from', 'date_to'])
64         res_leaves = resource_cal_leaves.browse(cr, uid, resource_leave_ids)
65
66         for leave in res_leaves:
67             dtf = datetime.strptime(leave.date_from, '%Y-%m-%d %H:%M:%S')
68             dtt = datetime.strptime(leave.date_to, '%Y-%m-%d %H:%M:%S')
69             no = dtt - dtf
70             [dt_leave.append((dtf + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
71             dt_leave.sort()
72
73         return dt_leave
74
75     def interval_min_get(self, cr, uid, id, dt_from, hours, resource=False):
76         if not id:
77             td = int(hours)*3
78             return [(dt_from - timedelta(hours=td), dt_from)]
79         dt_leave = self._get_leaves(cr, uid, id, resource)
80         dt_leave.reverse()
81         todo = hours
82         result = []
83         maxrecur = 100
84         current_hour = dt_from.hour
85         while (todo>0) and maxrecur:
86             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))
87             for (hour_from,hour_to) in cr.fetchall():
88                 leave_flag  = False
89                 if (hour_from<current_hour) and (todo>0):
90                     m = min(hour_to, current_hour)
91                     if (m-hour_from)>todo:
92                         hour_from = m-todo
93                     dt_check = dt_from.strftime('%Y-%m-%d')
94                     for leave in dt_leave:
95                         if dt_check == leave:
96                             dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
97                             leave_flag = True
98                     if leave_flag:
99                         break
100                     else:
101                         d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_from)), int((hour_from%1) * 60))
102                         d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
103                         result.append((d1, d2))
104                         current_hour = hour_from
105                         todo -= (m-hour_from)
106             dt_from -= timedelta(days=1)
107             current_hour = 24
108             maxrecur -= 1
109         result.reverse()
110         return result
111
112     # def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
113     def interval_get_multi(self, cr, uid, date_and_hours_by_cal, resource=False, byday=True):
114         def group(lst, key):
115             lst.sort(key=itemgetter(key))
116             grouped = groupby(lst, itemgetter(key))
117             return dict([(k, [v for v in itr]) for k, itr in grouped])
118         # END group
119
120         cr.execute("select calendar_id, dayofweek, hour_from, hour_to from resource_calendar_attendance order by hour_from")
121         hour_res = cr.dictfetchall()
122         hours_by_cal = group(hour_res, 'calendar_id')
123
124         results = {}
125
126         for d, hours, id in date_and_hours_by_cal:
127             dt_from = datetime.strptime(d, '%Y-%m-%d %H:%M:%S')
128             if not id:
129                 td = int(hours)*3
130                 results[(d, hours, id)] = [(dt_from, dt_from + timedelta(hours=td))]
131                 continue
132
133             dt_leave = self._get_leaves(cr, uid, id, resource)
134             todo = hours
135             result = []
136             maxrecur = 100
137             current_hour = dt_from.hour
138             while (todo>0) and maxrecur:
139                 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())]:
140                     leave_flag  = False
141                     if (hour_to>current_hour) and (todo>0):
142                         m = max(hour_from, current_hour)
143                         if (hour_to-m)>todo:
144                             hour_to = m+todo
145                         dt_check = dt_from.strftime('%Y-%m-%d')
146                         for leave in dt_leave:
147                             if dt_check == leave:
148                                 dt_check = datetime.strptime(dt_check, '%Y-%m-%d') + timedelta(days=1)
149                                 leave_flag = True
150                         if leave_flag:
151                             break
152                         else:
153                             d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(m)), int((m%1) * 60))
154                             d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
155                             result.append((d1, d2))
156                             current_hour = hour_to
157                             todo -= (hour_to - m)
158                 dt_from += timedelta(days=1)
159                 current_hour = 0
160                 maxrecur -= 1
161             results[(d, hours, id)] = result
162         return results
163
164     def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True):
165         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)]
166         return res
167
168     def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False):
169         if not id:
170             return 0.0
171         dt_leave = self._get_leaves(cr, uid, id, resource)
172         hours = 0.0
173
174         current_hour = dt_from.hour
175
176         while (dt_from <= dt_to):
177             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))
178             der =  cr.fetchall()
179             for (hour_from,hour_to) in der:
180                 if hours != 0.0:#For first time of the loop only,hours will be 0
181                     current_hour = hour_from
182                 leave_flag = False
183                 if (hour_to>=current_hour):
184                     dt_check = dt_from.strftime('%Y-%m-%d')
185                     for leave in dt_leave:
186                         if dt_check == leave:
187                             dt_check = datetime.strptime(dt_check, "%Y-%m-%d") + timedelta(days=1)
188                             leave_flag = True
189
190                     if leave_flag:
191                         break
192                     else:
193                         d1 = dt_from
194                         d2 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(hour_to)), int((hour_to%1) * 60))
195
196                         if hours != 0.0:#For first time of the loop only,hours will be 0
197                             d1 = datetime(dt_from.year, dt_from.month, dt_from.day, int(math.floor(current_hour)), int((current_hour%1) * 60))
198
199                         if dt_from.day == dt_to.day:
200                             if hour_from <= dt_to.hour <= hour_to:
201                                 d2 = dt_to
202                         dt_from = d2
203                         hours += (d2-d1).seconds
204             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)
205             current_hour = 0.0
206
207         return (hours/3600)
208
209 resource_calendar()
210
211 class resource_calendar_attendance(osv.osv):
212     _name = "resource.calendar.attendance"
213     _description = "Work Detail"
214     _columns = {
215         'name' : fields.char("Name", size=64, required=True),
216         'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of week'),
217         'date_from' : fields.date('Starting date'),
218         'hour_from' : fields.float('Work from', size=8, required=True, help="Working time will start from"),
219         'hour_to' : fields.float("Work to", size=8, required=True, help="Working time will end at"),
220         'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True),
221     }
222     _order = 'dayofweek, hour_from'
223 resource_calendar_attendance()
224
225 def convert_timeformat(time_string):
226     split_list = str(time_string).split('.')
227     hour_part = split_list[0]
228     mins_part = split_list[1]
229     round_mins = int(round(float(mins_part) * 60,-2))
230     converted_string = hour_part + ':' + str(round_mins)[0:2]
231     return converted_string
232
233 class resource_resource(osv.osv):
234     _name = "resource.resource"
235     _description = "Resource Detail"
236     _columns = {
237         'name' : fields.char("Name", size=64, required=True),
238         'code': fields.char('Code', size=16),
239         '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."),
240         'company_id' : fields.many2one('res.company', 'Company'),
241         'resource_type': fields.selection([('user','Human'),('material','Material')], 'Resource Type', required=True),
242         'user_id' : fields.many2one('res.users', 'User', help='Related user name for the resource to manage its access.'),
243         '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%."),
244         'calendar_id' : fields.many2one("resource.calendar", "Working Time", help="Define the schedule of resource"),
245     }
246     _defaults = {
247         'resource_type' : 'user',
248         'time_efficiency' : 1,
249         'active' : True,
250         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context)
251     }
252
253     
254     def copy(self, cr, uid, id, default=None, context=None):
255         if default is None:
256             default = {}
257         if not default.get('name', False):
258             default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
259         return super(resource_resource, self).copy(cr, uid, id, default, context)
260
261     def generate_resources(self, cr, uid, user_ids, calendar_id, context=None):
262         """
263         Return a list of  Resource Class objects for the resources allocated to the phase.
264         """
265         resource_objs = {}
266         user_pool = self.pool.get('res.users')
267         for user in user_pool.browse(cr, uid, user_ids, context=context):
268             resource_ids = self.search(cr, uid, [('user_id', '=', user.id)], context=context)
269             #assert len(resource_ids) < 1, "User should not has more than one resources"
270             leaves = []
271             resource_eff = 1.0
272             if resource_ids:
273                 for resource in self.browse(cr, uid, resource_ids, context=context):
274                     resource_eff = resource.time_efficiency
275                     resource_cal = resource.calendar_id.id
276                     if resource_cal:
277                         leaves = self.compute_vacation(cr, uid, calendar_id, resource.id, resource_cal, context=context)
278                     temp = {
279                              'name' : resource.name,
280                              'vacation': tuple(leaves),
281                              'efficiency': resource_eff,
282                           }
283                     resource_objs[resource.id] = temp     
284 #            resource_objs.append(classobj(str(user.name), (Resource,),{
285 #                                             '__doc__': user.name,
286 #                                             '__name__': user.name,
287 #                                             'vacation': tuple(leaves),
288 #                                             'efficiency': resource_eff,
289 #                                          }))
290         return resource_objs
291
292     def compute_vacation(self, cr, uid, calendar_id, resource_id=False, resource_calendar=False, context=None):
293         """
294         Compute the vacation from the working calendar of the resource.
295         @param calendar_id : working calendar of the project
296         @param resource_id : resource working on phase/task
297         @param resource_calendar : working calendar of the resource
298         """
299         resource_calendar_leaves_pool = self.pool.get('resource.calendar.leaves')
300         leave_list = []
301         if resource_id:
302             leave_ids = resource_calendar_leaves_pool.search(cr, uid, ['|', ('calendar_id', '=', calendar_id),
303                                                                        ('calendar_id', '=', resource_calendar),
304                                                                        ('resource_id', '=', resource_id)
305                                                                       ], context=context)
306         else:
307             leave_ids = resource_calendar_leaves_pool.search(cr, uid, [('calendar_id', '=', calendar_id),
308                                                                       ('resource_id', '=', False)
309                                                                       ], context=context)
310         leaves = resource_calendar_leaves_pool.read(cr, uid, leave_ids, ['date_from', 'date_to'], context=context)
311         for i in range(len(leaves)):
312             dt_start = datetime.strptime(leaves[i]['date_from'], '%Y-%m-%d %H:%M:%S')
313             dt_end = datetime.strptime(leaves[i]['date_to'], '%Y-%m-%d %H:%M:%S')
314             no = dt_end - dt_start
315             [leave_list.append((dt_start + timedelta(days=x)).strftime('%Y-%m-%d')) for x in range(int(no.days + 1))]
316             leave_list.sort()
317         return leave_list
318
319     def compute_working_calendar(self, cr, uid, calendar_id=False, context=None):
320         """
321         Change the format of working calendar from 'Openerp' format to bring it into 'Faces' format.
322         @param calendar_id : working calendar of the project
323         """
324         if not calendar_id:
325             # Calendar is not specified: working days: 24/7
326             return [('fri', '1:0-12:0','12:0-24:0'), ('thu', '1:0-12:0','12:0-24:0'), ('wed', '1:0-12:0','12:0-24:0'), 
327                    ('mon', '1:0-12:0','12:0-24:0'), ('tue', '1:0-12:0','12:0-24:0'), ('sat', '1:0-12:0','12:0-24:0'), ('sun', '1:0-12:0','12:0-24:0')]
328         resource_attendance_pool = self.pool.get('resource.calendar.attendance')
329         time_range = "8:00-8:00"
330         non_working = ""
331         week_days = {"0": "mon", "1": "tue", "2": "wed","3": "thu", "4": "fri", "5": "sat", "6": "sun"}
332         wk_days = {}
333         wk_time = {}
334         wktime_list = []
335         wktime_cal = []
336         week_ids = resource_attendance_pool.search(cr, uid, [('calendar_id', '=', calendar_id)], context=context)
337         weeks = resource_attendance_pool.read(cr, uid, week_ids, ['dayofweek', 'hour_from', 'hour_to'], context=context)
338         # Convert time formats into appropriate format required
339         # and create a list like [('mon', '8:00-12:00'), ('mon', '13:00-18:00')]
340         for week in weeks:
341             res_str = ""
342             day = None
343             if week_days.has_key(week['dayofweek']):
344                 day = week_days[week['dayofweek']]
345                 wk_days[week['dayofweek']] = week_days[week['dayofweek']]
346             hour_from_str = convert_timeformat(week['hour_from'])
347             hour_to_str = convert_timeformat(week['hour_to'])
348             res_str = hour_from_str + '-' + hour_to_str
349             wktime_list.append((day, res_str))
350         # Convert into format like [('mon', '8:00-12:00', '13:00-18:00')]
351         for item in wktime_list:
352             if wk_time.has_key(item[0]):
353                 wk_time[item[0]].append(item[1])
354             else:
355                 wk_time[item[0]] = [item[0]]
356                 wk_time[item[0]].append(item[1])
357         for k,v in wk_time.items():
358             wktime_cal.append(tuple(v))
359         # Add for the non-working days like: [('sat, sun', '8:00-8:00')]
360         for k, v in wk_days.items():
361             if week_days.has_key(k):
362                 week_days.pop(k)
363         for v in week_days.itervalues():
364             non_working += v + ','
365         if non_working:
366             wktime_cal.append((non_working[:-1], time_range))
367         return wktime_cal
368
369     #TODO: Write optimized alogrothem for resource availability. : Method Yet not implemented
370     def check_availability(self, cr, uid, ids, start, end, context=None):
371         if context ==  None:
372             contex = {}
373         allocation = {}
374         return allocation
375
376 resource_resource()
377
378 class resource_calendar_leaves(osv.osv):
379     _name = "resource.calendar.leaves"
380     _description = "Leave Detail"
381     _columns = {
382         'name' : fields.char("Name", size=64),
383         'company_id' : fields.related('calendar_id','company_id',type='many2one',relation='res.company',string="Company", store=True, readonly=True),
384         'calendar_id' : fields.many2one("resource.calendar", "Working time"),
385         'date_from' : fields.datetime('Start Date', required=True),
386         'date_to' : fields.datetime('End Date', required=True),
387         '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"),
388     }
389
390     def check_dates(self, cr, uid, ids, context=None):
391          leave = self.read(cr, uid, ids[0], ['date_from', 'date_to'])
392          if leave['date_from'] and leave['date_to']:
393              if leave['date_from'] > leave['date_to']:
394                  return False
395          return True
396
397     _constraints = [
398         (check_dates, 'Error! leave start-date must be lower then leave end-date.', ['date_from', 'date_to'])
399     ]
400
401     def onchange_resource(self,cr, uid, ids, resource, context=None):
402         result = {}
403         if resource:
404             resource_pool = self.pool.get('resource.resource')
405             result['calendar_id'] = resource_pool.browse(cr, uid, resource, context=context).calendar_id.id
406             return {'value': result}
407         return {'value': {'calendar_id': []}}
408
409 resource_calendar_leaves()
410
411 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: