[REF] hr_*
[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         obj_crm_meeting = self.pool.get('crm.meeting')
239         for record in self.browse(cr, uid, ids):
240             if record.state=='validate':
241                 if record.case_id:
242                     obj_crm_meeting.unlink(cr, uid, [record.case_id.id])
243                 if record.linked_request_ids:
244                     list_ids = [ lr.id for lr 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         obj_hr_holiday_status = self.pool.get('hr.holidays.status')
363         for record in self.browse(cr, uid, ids):
364             user_id = False
365             leave_asked = record.number_of_days_temp
366             if record.holiday_type == 'employee' and record.type == 'remove':
367                 if record.employee_id and not record.holiday_status_id.limit:
368                     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']
369                     if leaves_rest < leave_asked:
370                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for %s while available leaves are less than asked leaves.' %(record.employee_id.name)))
371                 nb = -(record.number_of_days_temp)
372             elif record.holiday_type == 'category' and record.type == 'remove':
373                 if record.category_id and not record.holiday_status_id.limit:
374                     leaves_rest = obj_hr_holiday_status.get_days_cat( cr, uid, [record.holiday_status_id.id], record.category_id.id, False)[record.holiday_status_id.id]['remaining_leaves']
375                     if leaves_rest < leave_asked:
376                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for %s while available leaves are less than asked leaves.' %(record.category_id.name)))
377                 nb = -(record.number_of_days_temp)
378             else:
379                 nb = record.number_of_days_temp
380
381             if record.holiday_type == 'employee' and record.employee_id:
382                 user_id = record.employee_id.user_id and record.employee_id.user_id.id or uid
383
384             self.write(cr, uid, [record.id], {'state':'confirm', 'number_of_days': nb, 'user_id': user_id })
385         return True
386
387     def holidays_refuse(self, cr, uid, ids, *args):
388         vals = {'state': 'refuse'}
389         ids2 = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)])
390         if ids2:
391             vals['manager_id'] = ids2[0]
392         self.write(cr, uid, ids, vals)
393         return True
394
395     def holidays_cancel(self, cr, uid, ids, *args):
396         self._update_user_holidays(cr, uid, ids)
397         self.write(cr, uid, ids, {'state': 'cancel'})
398         self._remove_resouce_leave(cr, uid, ids)
399         return True
400
401     def holidays_draft(self, cr, uid, ids, *args):
402         self.write(cr, uid, ids, {'state': 'draft'})
403         return True
404
405     def check_holidays(self, cr, uid, ids):
406         holi_status_obj = self.pool.get('hr.holidays.status')
407         emp_obj = self.pool.get('hr.employee')
408         meeting_obj = self.pool.get('crm.meeting')
409         for record in self.browse(cr, uid, ids):
410             if not record.number_of_days:
411                 raise osv.except_osv(_('Warning!'), _('Wrong leave definition.'))
412             if record.holiday_type=='employee' and record.employee_id:
413                 leave_asked = record.number_of_days
414                 if leave_asked < 0.00:
415                     if not record.holiday_status_id.limit:
416                         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']
417                         if leaves_rest < -(leave_asked):
418                             raise osv.except_osv(_('Warning!'),_('You Cannot Validate leaves while available leaves are less than asked leaves.'))
419             elif record.holiday_type == 'category' and record.category_id:
420                 leave_asked = record.number_of_days
421                 if leave_asked < 0.00:
422                     if not record.holiday_status_id.limit:
423                         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']
424                         if leaves_rest < -(leave_asked):
425                             raise osv.except_osv(_('Warning!'),_('You Cannot Validate leaves while available leaves are less than asked leaves.'))
426             else:# This condition will never meet!!
427                 holiday_ids = []
428                 vals = {
429                     'name': record.name,
430                     'holiday_status_id': record.holiday_status_id.id,
431                     'state': 'draft',
432                     'date_from': record.date_from,
433                     'date_to': record.date_to,
434                     'notes': record.notes,
435                     'number_of_days': record.number_of_days,
436                     'number_of_days_temp': record.number_of_days_temp,
437                     'type': record.type,
438                     'allocation_type': record.allocation_type,
439                     'parent_id': record.id,
440                 }
441                 employee_ids = emp_obj.search(cr, uid, [])
442                 for employee in employee_ids:
443                     vals['employee_id'] = employee
444                     user_id = emp_obj.search(cr, uid, [('user_id','=',uid)])
445                     if user_id:
446                         vals['user_id'] = user_id[0]
447                     holiday_ids.append(self.create(cr, uid, vals, context=None))
448                 self.holidays_confirm(cr, uid, holiday_ids)
449                 self.holidays_validate(cr, uid, holiday_ids)
450
451             if record.holiday_status_id.categ_id and record.date_from and record.date_to and record.employee_id:
452                 diff_day = self._get_number_of_days(record.date_from, record.date_to)
453                 vals = {
454                     'name': record.name,
455                     'categ_id': record.holiday_status_id.categ_id.id,
456                     'duration': (diff_day) * 8,
457                     'note': record.notes,
458                     'user_id': record.user_id.id,
459                     'date': record.date_from,
460                 }
461                 case_id = meeting_obj.create(cr, uid, vals)
462                 self.write(cr, uid, ids, {'case_id': case_id})
463
464         return True
465
466 hr_holidays()
467
468 class resource_calendar_leaves(osv.osv):
469     _inherit = "resource.calendar.leaves"
470     _description = "Leave Detail"
471     _columns = {
472         'holiday_id': fields.many2one("hr.holidays", "Holiday"),
473     }
474
475 resource_calendar_leaves()
476
477 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: