[IMP] better useability
[odoo/odoo.git] / addons / hr_holidays / hr_holidays.py
1 # -*- coding: utf-8 -*-
2 ##################################################################################
3 #
4 # Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
5 # and 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 # $Id: hr.py 4656 2006-11-24 09:58:42Z Cyp $
8 #
9 #     This program is free software: you can redistribute it and/or modify
10 #     it under the terms of the GNU Affero General Public License as
11 #     published by the Free Software Foundation, either version 3 of the
12 #     License, or (at your option) any later version.
13 #
14 #     This program is distributed in the hope that it will be useful,
15 #     but WITHOUT ANY WARRANTY; without even the implied warranty of
16 #     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 #     GNU Affero General Public License for more details.
18 #
19 #     You should have received a copy of the GNU Affero General Public License
20 #     along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22 ##############################################################################
23
24 import time
25 import datetime
26 from itertools import groupby
27 from operator import itemgetter
28
29 import netsvc
30 from osv import fields, osv
31 from tools.translate import _
32
33
34 class hr_holidays_status(osv.osv):
35     _name = "hr.holidays.status"
36     _description = "Leave Type"
37
38     def get_days_cat(self, cr, uid, ids, category_id, return_false, context=None):
39         if context is None:
40             context = {}
41
42         cr.execute("""SELECT id, type, number_of_days, holiday_status_id FROM hr_holidays WHERE category_id = %s AND state='validate' AND holiday_status_id in %s""",
43             [category_id, tuple(ids)])
44         result = sorted(cr.dictfetchall(), key=lambda x: x['holiday_status_id'])
45
46         grouped_lines = dict((k, [v for v in itr]) for k, itr in groupby(result, itemgetter('holiday_status_id')))
47
48         res = {}
49         for record in self.browse(cr, uid, ids, context=context):
50             res[record.id] = {}
51             max_leaves = leaves_taken = 0
52             if not return_false:
53                 if record.id in grouped_lines:
54                     leaves_taken = -sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'remove'])
55                     max_leaves = sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'add'])
56
57             res[record.id]['max_leaves'] = max_leaves
58             res[record.id]['leaves_taken'] = leaves_taken
59             res[record.id]['remaining_leaves'] = max_leaves - leaves_taken
60
61         return res
62
63     def get_days(self, cr, uid, ids, employee_id, return_false, context=None):
64         if context is None:
65             context = {}
66
67         cr.execute("""SELECT id, type, number_of_days, holiday_status_id FROM hr_holidays WHERE employee_id = %s AND state='validate' AND holiday_status_id in %s""",
68             [employee_id, tuple(ids)])
69         result = sorted(cr.dictfetchall(), key=lambda x: x['holiday_status_id'])
70
71         grouped_lines = dict((k, [v for v in itr]) for k, itr in groupby(result, itemgetter('holiday_status_id')))
72
73         res = {}
74         for record in self.browse(cr, uid, ids, context=context):
75             res[record.id] = {}
76             max_leaves = leaves_taken = 0
77             if not return_false:
78                 if record.id in grouped_lines:
79                     leaves_taken = -sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'remove'])
80                     max_leaves = sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'add'])
81
82             res[record.id]['max_leaves'] = max_leaves
83             res[record.id]['leaves_taken'] = leaves_taken
84             res[record.id]['remaining_leaves'] = max_leaves - leaves_taken
85
86         return res
87
88     def _user_left_days(self, cr, uid, ids, name, args, context=None):
89         if context is None:
90             context = {}
91         return_false = False
92         employee_id = False
93         res = {}
94
95         if context and context.has_key('employee_id'):
96             if not context['employee_id']:
97                 return_false = True
98             employee_id = context['employee_id']
99         else:
100             employee_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
101             if employee_ids:
102                 employee_id = employee_ids[0]
103             else:
104                 return_false = True
105         if employee_id:
106             res = self.get_days(cr, uid, ids, employee_id, return_false, context=context)
107         else:
108             res = dict.fromkeys(ids, {'leaves_taken': 0, 'remaining_leaves': 0, 'max_leaves': 0})
109         return res
110
111     # To do: we can add remaining_leaves_category field to display remaining leaves for particular type
112     _columns = {
113         'name': fields.char('Leave Type', size=64, required=True, translate=True),
114         'categ_id': fields.many2one('crm.case.categ', 'Meeting Category', domain="[('object_id.model', '=', 'crm.meeting')]", help='If you link this type of leave with a category in the CRM, it will synchronize each leave asked with a case in this category, to display it in the company shared calendar for example.'),
115         'color_name': fields.selection([('red', 'Red'),('blue','Blue'), ('lightgreen', 'Light Green'), ('lightblue','Light Blue'), ('lightyellow', 'Light Yellow'), ('magenta', 'Magenta'),('lightcyan', 'Light Cyan'),('black', 'Black'),('lightpink', 'Light Pink'),('brown', 'Brown'),('violet', 'Violet'),('lightcoral', 'Light Coral'),('lightsalmon', 'Light Salmon'),('lavender', 'Lavender'),('wheat', 'Wheat'),('ivory', 'Ivory')],'Color in Report', required=True, help='This color will be used in the leaves summary located in Reporting\Leaves by Departement'),
116         'limit': fields.boolean('Allow to Override Limit', help='If you tick this checkbox, the system will allow, for this section, the employees to take more leaves than the available ones.'),
117         'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the leave type without removing it."),
118         'max_leaves': fields.function(_user_left_days, method=True, string='Maximum Leaves Allowed', help='This value is given by the sum of all holidays requests with a positive value.', multi='user_left_days'),
119         'leaves_taken': fields.function(_user_left_days, method=True, string='Leaves Already Taken', help='This value is given by the sum of all holidays requests with a negative value.', multi='user_left_days'),
120         'remaining_leaves': fields.function(_user_left_days, method=True, string='Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken', multi='user_left_days'),
121         'double_validation': fields.boolean('Apply Double Validation', help="If its True then its Allocation/Request have to be validated by second validator")
122     }
123     _defaults = {
124         'color_name': 'red',
125         'active': True,
126     }
127
128 hr_holidays_status()
129
130 class hr_holidays(osv.osv):
131     _name = "hr.holidays"
132     _description = "Leave"
133     _order = "type desc, date_from asc"
134
135     def _employee_get(obj, cr, uid, context=None):
136         if context is None:
137             context = {}
138         ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
139         if ids:
140             return ids[0]
141         return False
142
143     _columns = {
144         'name': fields.char('Description', required=True, size=64),
145         'state': fields.selection([('draft', 'Draft'), ('confirm', 'Waiting Approval'), ('refuse', 'Refused'), ('validate1', 'Waiting Second Approval'), ('validate', 'Approved'), ('cancel', 'Cancelled')], 'State', readonly=True, help='When the holiday request is created the state is \'Draft\'.\n It is confirmed by the user and request is sent to admin, the state is \'Waiting Approval\'.\
146             If the admin accepts it, the state is \'Approved\'. If it is refused, the state is \'Refused\'.'),
147         'date_from': fields.datetime('Start Date', readonly=True, states={'draft':[('readonly',False)]}),
148         'user_id':fields.many2one('res.users', 'User', states={'draft':[('readonly',False)]}, select=True, readonly=True),
149         'date_to': fields.datetime('End Date', readonly=True, states={'draft':[('readonly',False)]}),
150         'holiday_status_id' : fields.many2one("hr.holidays.status", " Leave Type", required=True,readonly=True, states={'draft':[('readonly',False)]}),
151         'employee_id' : fields.many2one('hr.employee', "Employee", select=True, invisible=False, readonly=True, states={'draft':[('readonly',False)]}, help='Leave Manager can let this field empty if this leave request/allocation is for every employee'),
152         #'manager_id': fields.many2one('hr.employee', 'Leave Manager', invisible=False, readonly=True, help='This area is automaticly filled by the user who validate the leave'),
153         #'notes': fields.text('Notes',readonly=True, states={'draft':[('readonly',False)]}),
154         'manager_id': fields.many2one('hr.employee', 'First Approval', invisible=False, readonly=True, help='This area is automaticly filled by the user who validate the leave'),
155         'notes': fields.text('Reasons',readonly=True, states={'draft':[('readonly',False)]}),
156         'number_of_days': fields.float('Number of Days', readonly=True, states={'draft':[('readonly',False)]}),
157         'number_of_days_temp': fields.float('Number of Days', readonly=True, states={'draft':[('readonly',False)]}),
158         'case_id': fields.many2one('crm.meeting', 'Case'),
159         'type': fields.selection([('remove','Leave Request'),('add','Allocation Request')], 'Request Type', required=True, readonly=True, states={'draft':[('readonly',False)]}, help="Choose 'Leave Request' if someone wants to take an off-day. \nChoose 'Allocation Request' if you want to increase the number of leaves available for someone"),
160         'allocation_type': fields.selection([('employee','Employee Request'),('company','Company Allocation')], 'Allocation Type', required=True, readonly=True, states={'draft':[('readonly',False)]}, help='This field is only for informative purposes, to depict if the leave request/allocation comes from an employee or from the company'),
161         'parent_id': fields.many2one('hr.holidays', 'Parent'),
162         'linked_request_ids': fields.one2many('hr.holidays', 'parent_id', 'Linked Requests',),
163         'department_id':fields.related('employee_id', 'department_id', string='Department', type='many2one', relation='hr.department', readonly=True, store=True),
164         'category_id': fields.many2one('hr.employee.category', "Category", help='Category Of employee'),
165         'holiday_type': fields.selection([('employee','By Employee'),('category','By Employee Category')], 'Allocation Type', help='By Employee: Allocation/Request for individual Employee, By Employee Category: Allocation/Request for group of employees in category', required=True),
166         'manager_id2': fields.many2one('hr.employee', 'Second Approval', readonly=True, help='This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)')
167     }
168
169     _defaults = {
170         'employee_id': _employee_get ,
171         'state': 'draft',
172         'type': 'remove',
173         'allocation_type': 'employee',
174         'user_id': lambda obj, cr, uid, context: uid,
175         'holiday_type': 'employee'
176     }
177
178     def _create_resource_leave(self, cr, uid, vals, context=None):
179         '''This method will create entry in resource calendar leave object at the time of holidays validated '''
180         if context is None:
181             context = {}
182         obj_res_leave = self.pool.get('resource.calendar.leaves')
183         return obj_res_leave.create(cr, uid, vals, context=context)
184
185     def _remove_resouce_leave(self, cr, uid, ids, context=None):
186         '''This method will create entry in resource calendar leave object at the time of holidays cancel/removed'''
187         obj_res_leave = self.pool.get('resource.calendar.leaves')
188         if context is None:
189             context = {}
190         leave_ids = obj_res_leave.search(cr, uid, [('holiday_id', 'in', ids)], context=context)
191         return obj_res_leave.unlink(cr, uid, leave_ids)
192
193     def create(self, cr, uid, vals, context=None):
194         if context is None:
195             context = {}
196         if 'holiday_type' in vals:
197             if vals['holiday_type'] == 'employee':
198                 vals.update({'category_id': False})
199             else:
200                 vals.update({'employee_id': False})
201         if context.has_key('type'):
202             vals['type'] = context['type']
203         if context.has_key('allocation_type'):
204             vals['allocation_type'] = context['allocation_type']
205         return super(hr_holidays, self).create(cr, uid, vals, context=context)
206
207     def write(self, cr, uid, ids, vals, context=None):
208         if context is None:
209             context = {}
210         if 'holiday_type' in vals:
211             if vals['holiday_type'] == 'employee':
212                 vals.update({'category_id': False})
213             else:
214                 vals.update({'employee_id': False})
215         return super(hr_holidays, self).write(cr, uid, ids, vals, context=context)
216
217     def onchange_type(self, cr, uid, ids, holiday_type):
218         result = {}
219         if holiday_type=='employee':
220             ids_employee = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)])
221             if ids_employee:
222                 result['value'] = {
223                     'employee_id': ids_employee[0]
224                                     }
225         return result
226
227     def _get_number_of_days(self, date_from, date_to):
228         """Returns a float equals to the timedelta between two dates given as string."""
229
230         DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
231         from_dt = datetime.datetime.strptime(date_from, DATETIME_FORMAT)
232         to_dt = datetime.datetime.strptime(date_to, DATETIME_FORMAT)
233         timedelta = to_dt - from_dt
234         diff_day = timedelta.days + float(timedelta.seconds) / 86400
235         return diff_day
236
237     def _update_user_holidays(self, cr, uid, ids):
238         for record in self.browse(cr, uid, ids):
239             if record.state=='validate':
240                 if record.case_id:
241                     self.pool.get('crm.meeting').unlink(cr, uid, [record.case_id.id])
242                 if record.linked_request_ids:
243                     list_ids = []
244                     [list_ids.append(i) for id in record.linked_request_ids]
245                     self.holidays_cancel(cr, uid, list_ids)
246                     self.unlink(cr, uid, list_ids)
247
248     def _check_date(self, cr, uid, ids):
249         for rec in self.read(cr, uid, ids, ['number_of_days_temp', 'date_from','date_to', 'type']):
250             if rec['number_of_days_temp'] < 0:
251                 return False
252             if rec['type'] == 'add':
253                 continue
254             date_from = time.strptime(rec['date_from'], '%Y-%m-%d %H:%M:%S')
255             date_to = time.strptime(rec['date_to'], '%Y-%m-%d %H:%M:%S')
256             if date_from > date_to:
257                 return False
258         return True
259
260     _constraints = [(_check_date, 'Start date should not be larger than end date!\nNumber of Days should be greater than 1!', ['number_of_days_temp'])]
261
262     def unlink(self, cr, uid, ids, context=None):
263         if context is None:
264             context = {}
265         self._update_user_holidays(cr, uid, ids)
266         self._remove_resouce_leave(cr, uid, ids, context=context)
267         return super(hr_holidays, self).unlink(cr, uid, ids, context)
268
269     def onchange_date_from(self, cr, uid, ids, date_to, date_from):
270         result = {}
271         if date_to and date_from:
272             diff_day = self._get_number_of_days(date_from, date_to)
273             result['value'] = {
274                 'number_of_days_temp': round(diff_day)+1
275             }
276             return result
277         result['value'] = {
278             'number_of_days_temp': 0,
279         }
280         return result
281
282     def onchange_date_to(self, cr, uid, ids, date_from, date_to):
283         return self.onchange_date_from(cr, uid, ids, date_to, date_from)
284
285     def onchange_sec_id(self, cr, uid, ids, status, context=None):
286         if context is None:
287             context = {}
288         warning = {}
289         if status:
290             brows_obj = self.pool.get('hr.holidays.status').browse(cr, uid, [status], context=context)[0]
291             if brows_obj.categ_id and brows_obj.categ_id.section_id and not brows_obj.categ_id.section_id.allow_unlink:
292                 warning = {
293                     'title': "Warning for ",
294                     'message': "You won\'t be able to cancel this leave request because the CRM Sales Team of the leave type disallows."
295                         }
296         return {'warning': warning}
297
298     def set_to_draft(self, cr, uid, ids, *args):
299         wf_service = netsvc.LocalService("workflow")
300         self.write(cr, uid, ids, {
301             'state': 'draft',
302             'manager_id': False,
303             'number_of_days': 0,
304         })
305         for id in ids:
306             wf_service.trg_create(uid, 'hr.holidays', id, cr)
307         return True
308
309     def holidays_validate2(self, cr, uid, ids, *args):
310         vals = {'state':'validate1'}
311         self.check_holidays(cr, uid, ids)
312         ids2 = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)])
313         if ids2:
314             vals['manager_id'] = ids2[0]
315         else:
316             raise osv.except_osv(_('Warning !'),_('No user related to the selected employee.'))
317         self.write(cr, uid, ids, vals)
318         return True
319
320     def holidays_validate(self, cr, uid, ids, *args):
321         obj_emp = self.pool.get('hr.employee')
322         data_holiday = self.browse(cr, uid, ids)
323         self.check_holidays(cr, uid, ids)
324         vals = {'state':'validate'}
325         ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)])
326         if ids2:
327             if data_holiday[0].state == 'validate1':
328                 vals['manager_id2'] = ids2[0]
329             else:
330                 vals['manager_id'] = ids2[0]
331         else:
332             raise osv.except_osv(_('Warning !'), _('No user related to the selected employee.'))
333         self.write(cr, uid, ids, vals)
334         for record in data_holiday:
335             if record.holiday_type == 'employee' and record.type == 'remove':
336                 vals = {
337                    'name': record.name,
338                    'date_from': record.date_from,
339                    'date_to': record.date_to,
340                    'calendar_id': record.employee_id.calendar_id.id,
341                    'company_id': record.employee_id.company_id.id,
342                    'resource_id': record.employee_id.resource_id.id,
343                    'holiday_id': record.id
344                      }
345                 self._create_resource_leave(cr, uid, vals)
346             elif record.holiday_type == 'category' and record.type == 'remove':
347                 emp_ids = obj_emp.search(cr, uid, [('category_ids', '=', record.category_id.id)])
348                 for emp in obj_emp.browse(cr, uid, emp_ids):
349                     vals = {
350                        'name': record.name,
351                        'date_from': record.date_from,
352                        'date_to': record.date_to,
353                        'calendar_id': emp.calendar_id.id,
354                        'company_id': emp.company_id.id,
355                        'resource_id': emp.resource_id.id,
356                        'holiday_id':record.id
357                          }
358                     self._create_resource_leave(cr, uid, vals)
359         return True
360
361     def holidays_confirm(self, cr, uid, ids, *args):
362         for record in self.browse(cr, uid, ids):
363             user_id = False
364             leave_asked = record.number_of_days_temp
365             if record.holiday_type == 'employee' and record.type == 'remove':
366                 if record.employee_id and not record.holiday_status_id.limit:
367                     leaves_rest = self.pool.get('hr.holidays.status').get_days( cr, uid, [record.holiday_status_id.id], record.employee_id.id, False)[record.holiday_status_id.id]['remaining_leaves']
368                     if leaves_rest < leave_asked:
369                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for %s while available leaves are less than asked leaves.' %(record.employee_id.name)))
370                 nb = -(record.number_of_days_temp)
371             elif record.holiday_type == 'category' and record.type == 'remove':
372                 if record.category_id and not record.holiday_status_id.limit:
373                     leaves_rest = self.pool.get('hr.holidays.status').get_days_cat( cr, uid, [record.holiday_status_id.id], record.category_id.id, False)[record.holiday_status_id.id]['remaining_leaves']
374                     if leaves_rest < leave_asked:
375                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for %s while available leaves are less than asked leaves.' %(record.category_id.name)))
376                 nb = -(record.number_of_days_temp)
377             else:
378                 nb = record.number_of_days_temp
379
380             if record.holiday_type == 'employee' and record.employee_id:
381                 user_id = record.employee_id.user_id and record.employee_id.user_id.id or uid
382
383             self.write(cr, uid, [record.id], {'state':'confirm', 'number_of_days': nb, 'user_id': user_id })
384         return True
385
386     def holidays_refuse(self, cr, uid, ids, *args):
387         vals = {'state': 'refuse'}
388         ids2 = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)])
389         if ids2:
390             vals['manager_id'] = ids2[0]
391         self.write(cr, uid, ids, vals)
392         return True
393
394     def holidays_cancel(self, cr, uid, ids, *args):
395         self._update_user_holidays(cr, uid, ids)
396         self.write(cr, uid, ids, {'state': 'cancel'})
397         self._remove_resouce_leave(cr, uid, ids)
398         return True
399
400     def holidays_draft(self, cr, uid, ids, *args):
401         self.write(cr, uid, ids, {'state': 'draft'})
402         return True
403
404     def check_holidays(self, cr, uid, ids):
405         holi_status_obj = self.pool.get('hr.holidays.status')
406         emp_obj = self.pool.get('hr.employee')
407         meeting_obj = self.pool.get('crm.meeting')
408         for record in self.browse(cr, uid, ids):
409             if not record.number_of_days:
410                 raise osv.except_osv(_('Warning!'), _('Wrong leave definition.'))
411             if record.holiday_type=='employee' and record.employee_id:
412                 leave_asked = record.number_of_days
413                 if leave_asked < 0.00:
414                     if not record.holiday_status_id.limit:
415                         leaves_rest = holi_status_obj.get_days(cr, uid, [record.holiday_status_id.id], record.employee_id.id, False)[record.holiday_status_id.id]['remaining_leaves']
416                         if leaves_rest < -(leave_asked):
417                             raise osv.except_osv(_('Warning!'),_('You Cannot Validate leaves while available leaves are less than asked leaves.'))
418             elif record.holiday_type == 'category' and record.category_id:
419                 leave_asked = record.number_of_days
420                 if leave_asked < 0.00:
421                     if not record.holiday_status_id.limit:
422                         leaves_rest = holi_status_obj.get_days_cat(cr, uid, [record.holiday_status_id.id], record.category_id.id, False)[record.holiday_status_id.id]['remaining_leaves']
423                         if leaves_rest < -(leave_asked):
424                             raise osv.except_osv(_('Warning!'),_('You Cannot Validate leaves while available leaves are less than asked leaves.'))
425             else:# This condition will never meet!!
426                 holiday_ids = []
427                 vals = {
428                     'name' : record.name,
429                     'holiday_status_id' : record.holiday_status_id.id,
430                     'state': 'draft',
431                     'date_from' : record.date_from,
432                     'date_to' : record.date_to,
433                     'notes' : record.notes,
434                     'number_of_days': record.number_of_days,
435                     'number_of_days_temp': record.number_of_days_temp,
436                     'type': record.type,
437                     'allocation_type': record.allocation_type,
438                     'parent_id': record.id,
439                 }
440                 employee_ids = emp_obj.search(cr, uid, [])
441                 for employee in employee_ids:
442                     vals['employee_id'] = employee
443                     user_id = emp_obj.search(cr, uid, [('user_id','=',uid)])
444                     if user_id:
445                         vals['user_id'] = user_id[0]
446                     holiday_ids.append(self.create(cr, uid, vals, context=None))
447                 self.holidays_confirm(cr, uid, holiday_ids)
448                 self.holidays_validate(cr, uid, holiday_ids)
449
450             if record.holiday_status_id.categ_id and record.date_from and record.date_to and record.employee_id:
451                 diff_day = self._get_number_of_days(record.date_from, record.date_to)
452                 vals = {
453                     'name' : record.name,
454                     'categ_id' : record.holiday_status_id.categ_id.id,
455                     'duration' : (diff_day) * 8,
456                     'note' : record.notes,
457                     'user_id' : record.user_id.id,
458                     'date' : record.date_from,
459                 }
460                 case_id = meeting_obj.create(cr, uid, vals)
461                 self.write(cr, uid, ids, {'case_id': case_id})
462
463         return True
464
465 hr_holidays()
466
467 class resource_calendar_leaves(osv.osv):
468     _inherit = "resource.calendar.leaves"
469     _description = "Leave Detail"
470     _columns = {
471         'holiday_id': fields.many2one("hr.holidays", "Holiday"),
472     }
473
474 resource_calendar_leaves()
475
476 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: