[FIX] hr_holidays: fix domain in determination of employee leave status
[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 datetime, time
25 from itertools import groupby
26 from operator import itemgetter
27
28 import netsvc
29 from osv import fields, osv
30 from tools.translate import _
31
32
33 class hr_holidays_status(osv.osv):
34     _name = "hr.holidays.status"
35     _description = "Leave Type"
36
37     def get_days(self, cr, uid, ids, employee_id, return_false, context=None):
38         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""",
39             [employee_id, tuple(ids)])
40         result = sorted(cr.dictfetchall(), key=lambda x: x['holiday_status_id'])
41         grouped_lines = dict((k, [v for v in itr]) for k, itr in groupby(result, itemgetter('holiday_status_id')))
42         res = {}
43         for record in self.browse(cr, uid, ids, context=context):
44             res[record.id] = {}
45             max_leaves = leaves_taken = 0
46             if not return_false:
47                 if record.id in grouped_lines:
48                     leaves_taken = -sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'remove'])
49                     max_leaves = sum([item['number_of_days'] for item in grouped_lines[record.id] if item['type'] == 'add'])
50             res[record.id]['max_leaves'] = max_leaves
51             res[record.id]['leaves_taken'] = leaves_taken
52             res[record.id]['remaining_leaves'] = max_leaves - leaves_taken
53         return res
54
55     def _user_left_days(self, cr, uid, ids, name, args, context=None):
56         return_false = False
57         employee_id = False
58         res = {}
59         if context and context.has_key('employee_id'):
60             if not context['employee_id']:
61                 return_false = True
62             employee_id = context['employee_id']
63         else:
64             employee_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
65             if employee_ids:
66                 employee_id = employee_ids[0]
67             else:
68                 return_false = True
69         if employee_id:
70             res = self.get_days(cr, uid, ids, employee_id, return_false, context=context)
71         else:
72             res = dict.fromkeys(ids, {'leaves_taken': 0, 'remaining_leaves': 0, 'max_leaves': 0})
73         return res
74
75     _columns = {
76         'name': fields.char('Leave Type', size=64, required=True, translate=True),
77         'categ_id': fields.many2one('crm.case.categ', 'Meeting', domain="[('object_id.model', '=', 'crm.meeting')]", help='If you set a meeting type, OpenERP will create a meeting in the calendar once a leave is validated.'),
78         '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'),
79         '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.'),
80         '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."),
81         'max_leaves': fields.function(_user_left_days, string='Maximum Allowed', help='This value is given by the sum of all holidays requests with a positive value.', multi='user_left_days'),
82         'leaves_taken': fields.function(_user_left_days, string='Leaves Already Taken', help='This value is given by the sum of all holidays requests with a negative value.', multi='user_left_days'),
83         'remaining_leaves': fields.function(_user_left_days, string='Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken', multi='user_left_days'),
84         'double_validation': fields.boolean('Apply Double Validation', help="If its True then its Allocation/Request have to be validated by second validator")
85     }
86     _defaults = {
87         'color_name': 'red',
88         'active': True,
89     }
90 hr_holidays_status()
91
92 class hr_holidays(osv.osv):
93     _name = "hr.holidays"
94     _description = "Leave"
95     _order = "type desc, date_from asc"
96
97     def _employee_get(self, cr, uid, context=None):
98         ids = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
99         if ids:
100             return ids[0]
101         return False
102
103     def _compute_number_of_days(self, cr, uid, ids, name, args, context=None):
104         result = {}
105         for hol in self.browse(cr, uid, ids, context=context):
106             if hol.type=='remove':
107                 result[hol.id] = -hol.number_of_days_temp
108             else:
109                 result[hol.id] = hol.number_of_days_temp
110         return result
111
112     _columns = {
113         'name': fields.char('Description', required=True, size=64),
114         'state': fields.selection([('draft', 'New'), ('confirm', 'Waiting Approval'), ('refuse', 'Refused'),
115             ('validate1', 'Waiting Second Approval'), ('validate', 'Approved'), ('cancel', 'Cancelled')],
116             'State', readonly=True, help='The state is set to \'Draft\', when a holiday request is created.\
117             \nThe state is \'Waiting Approval\', when holiday request is confirmed by user.\
118             \nThe state is \'Refused\', when holiday request is refused by manager.\
119             \nThe state is \'Approved\', when holiday request is approved by manager.'),
120         'user_id':fields.related('employee_id', 'user_id', type='many2one', relation='res.users', string='User', store=True),
121         'date_from': fields.datetime('Start Date', readonly=True, states={'draft':[('readonly',False)]}, select=True),
122         'date_to': fields.datetime('End Date', readonly=True, states={'draft':[('readonly',False)]}),
123         'holiday_status_id': fields.many2one("hr.holidays.status", "Leave Type", required=True,readonly=True, states={'draft':[('readonly',False)]}),
124         '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'),
125         #'manager_id': fields.many2one('hr.employee', 'Leave Manager', invisible=False, readonly=True, help='This area is automatically filled by the user who validate the leave'),
126         #'notes': fields.text('Notes',readonly=True, states={'draft':[('readonly',False)]}),
127         'manager_id': fields.many2one('hr.employee', 'First Approval', invisible=False, readonly=True, help='This area is automatically filled by the user who validate the leave'),
128         'notes': fields.text('Reasons',readonly=True, states={'draft':[('readonly',False)]}),
129         'number_of_days_temp': fields.float('Number of Days', readonly=True, states={'draft':[('readonly',False)]}),
130         'number_of_days': fields.function(_compute_number_of_days, string='Number of Days', store=True),
131         'case_id': fields.many2one('crm.meeting', 'Meeting'),
132         '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", select=True),
133         'parent_id': fields.many2one('hr.holidays', 'Parent'),
134         'linked_request_ids': fields.one2many('hr.holidays', 'parent_id', 'Linked Requests',),
135         'department_id':fields.related('employee_id', 'department_id', string='Department', type='many2one', relation='hr.department', readonly=True, store=True),
136         'category_id': fields.many2one('hr.employee.category', "Category", help='Category of Employee'),
137         '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),
138         '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)'),
139         'double_validation': fields.related('holiday_status_id', 'double_validation', type='boolean', relation='hr.holidays.status', string='Apply Double Validation'),
140     }
141     _defaults = {
142         'employee_id': _employee_get,
143         'state': 'draft',
144         'type': 'remove',
145         'user_id': lambda obj, cr, uid, context: uid,
146         'holiday_type': 'employee'
147     }
148     _sql_constraints = [
149         ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL))", "You have to select an employee or a category"),
150         ('date_check2', "CHECK ( (type='add') OR (date_from <= date_to))", "The start date must be before the end date !"),
151         ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0 !"),
152     ]
153
154     def _create_resource_leave(self, cr, uid, leaves, context=None):
155         '''This method will create entry in resource calendar leave object at the time of holidays validated '''
156         obj_res_leave = self.pool.get('resource.calendar.leaves')
157         for leave in leaves:
158             vals = {
159                 'name': leave.name,
160                 'date_from': leave.date_from,
161                 'holiday_id': leave.id,
162                 'date_to': leave.date_to,
163                 'resource_id': leave.employee_id.resource_id.id,
164                 'calendar_id': leave.employee_id.resource_id.calendar_id.id
165             }
166             obj_res_leave.create(cr, uid, vals, context=context)
167         return True
168
169     def _remove_resource_leave(self, cr, uid, ids, context=None):
170         '''This method will create entry in resource calendar leave object at the time of holidays cancel/removed'''
171         obj_res_leave = self.pool.get('resource.calendar.leaves')
172         leave_ids = obj_res_leave.search(cr, uid, [('holiday_id', 'in', ids)], context=context)
173         return obj_res_leave.unlink(cr, uid, leave_ids, context=context)
174
175     def onchange_type(self, cr, uid, ids, holiday_type):
176         result = {'value': {'employee_id': False}}
177         if holiday_type == 'employee':
178             ids_employee = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)])
179             if ids_employee:
180                 result['value'] = {
181                     'employee_id': ids_employee[0]
182                 }
183         return result
184
185     # TODO: can be improved using resource calendar method
186     def _get_number_of_days(self, date_from, date_to):
187         """Returns a float equals to the timedelta between two dates given as string."""
188
189         DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
190         from_dt = datetime.datetime.strptime(date_from, DATETIME_FORMAT)
191         to_dt = datetime.datetime.strptime(date_to, DATETIME_FORMAT)
192         timedelta = to_dt - from_dt
193         diff_day = timedelta.days + float(timedelta.seconds) / 86400
194         return diff_day
195
196     def unlink(self, cr, uid, ids, context=None):
197         for rec in self.browse(cr, uid, ids, context=context):
198             if rec.state<>'draft':
199                 raise osv.except_osv(_('Warning!'),_('You cannot delete a leave which is not in draft state !'))
200         return super(hr_holidays, self).unlink(cr, uid, ids, context)
201
202     def onchange_date_from(self, cr, uid, ids, date_to, date_from):
203         result = {}
204         if date_to and date_from:
205             diff_day = self._get_number_of_days(date_from, date_to)
206             result['value'] = {
207                 'number_of_days_temp': round(diff_day)+1
208             }
209             return result
210         result['value'] = {
211             'number_of_days_temp': 0,
212         }
213         return result
214
215     def onchange_sec_id(self, cr, uid, ids, status, context=None):
216         warning = {}
217         double_validation = False
218         obj_holiday_status = self.pool.get('hr.holidays.status')
219         if status:
220             holiday_status = obj_holiday_status.browse(cr, uid, status, context=context)
221             double_validation = holiday_status.double_validation
222             if holiday_status.categ_id and holiday_status.categ_id.section_id and not holiday_status.categ_id.section_id.allow_unlink:
223                 warning = {
224                     'title': "Warning for ",
225                     'message': "You won\'t be able to cancel this leave request because the CRM Sales Team of the leave type disallows."
226                 }
227         return {'warning': warning, 'value': {'double_validation': double_validation}}
228
229     def set_to_draft(self, cr, uid, ids, context=None):
230         self.write(cr, uid, ids, {
231             'state': 'draft',
232             'manager_id': False,
233             'manager_id2': False,
234         })
235         wf_service = netsvc.LocalService("workflow")
236         for id in ids:
237             wf_service.trg_delete(uid, 'hr.holidays', id, cr)
238             wf_service.trg_create(uid, 'hr.holidays', id, cr)
239         return True
240
241     def holidays_validate(self, cr, uid, ids, context=None):
242         self.check_holidays(cr, uid, ids, context=context)
243         obj_emp = self.pool.get('hr.employee')
244         ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)])
245         manager = ids2 and ids2[0] or False
246         return self.write(cr, uid, ids, {'state':'validate1', 'manager_id': manager})
247
248     def holidays_validate2(self, cr, uid, ids, context=None):
249         self.check_holidays(cr, uid, ids, context=context)
250         obj_emp = self.pool.get('hr.employee')
251         ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)])
252         manager = ids2 and ids2[0] or False
253         self.write(cr, uid, ids, {'state':'validate'})
254         data_holiday = self.browse(cr, uid, ids)
255         holiday_ids = []
256         for record in data_holiday:
257             if record.holiday_status_id.double_validation:
258                 holiday_ids.append(record.id)
259             if record.holiday_type == 'employee' and record.type == 'remove':
260                 meeting_obj = self.pool.get('crm.meeting')
261                 vals = {
262                     'name': record.name,
263                     'categ_id': record.holiday_status_id.categ_id.id,
264                     'duration': record.number_of_days_temp * 8,
265                     'description': record.notes,
266                     'user_id': record.user_id.id,
267                     'date': record.date_from,
268                     'end_date': record.date_to,
269                     'date_deadline': record.date_to,
270                 }
271                 case_id = meeting_obj.create(cr, uid, vals)
272                 self._create_resource_leave(cr, uid, [record], context=context)
273                 self.write(cr, uid, ids, {'case_id': case_id})
274             elif record.holiday_type == 'category':
275                 emp_ids = obj_emp.search(cr, uid, [('category_ids', 'child_of', [record.category_id.id])])
276                 leave_ids = []
277                 for emp in obj_emp.browse(cr, uid, emp_ids):
278                     vals = {
279                         'name': record.name,
280                         'type': record.type,
281                         'holiday_type': 'employee',
282                         'holiday_status_id': record.holiday_status_id.id,
283                         'date_from': record.date_from,
284                         'date_to': record.date_to,
285                         'notes': record.notes,
286                         'number_of_days_temp': record.number_of_days_temp,
287                         'parent_id': record.id,
288                         'employee_id': emp.id
289                     }
290                     leave_ids.append(self.create(cr, uid, vals, context=None))
291                 wf_service = netsvc.LocalService("workflow")
292                 for leave_id in leave_ids:
293                     wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'confirm', cr)
294                     wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'validate', cr)
295                     wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'second_validate', cr)
296         if holiday_ids:
297             self.write(cr, uid, holiday_ids, {'manager_id2': manager})
298         return True
299
300     def holidays_confirm(self, cr, uid, ids, context=None):
301         self.check_holidays(cr, uid, ids, context=context)
302         return self.write(cr, uid, ids, {'state':'confirm'})
303
304     def holidays_refuse(self, cr, uid, ids, approval, context=None):
305         obj_emp = self.pool.get('hr.employee')
306         ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)])
307         manager = ids2 and ids2[0] or False
308         if approval == 'first_approval':
309             self.write(cr, uid, ids, {'state': 'refuse', 'manager_id': manager})
310         else:
311             self.write(cr, uid, ids, {'state': 'refuse', 'manager_id2': manager})
312         self.holidays_cancel(cr, uid, ids, context=context)
313         return True
314
315     def holidays_cancel(self, cr, uid, ids, context=None):
316         obj_crm_meeting = self.pool.get('crm.meeting')
317         for record in self.browse(cr, uid, ids):
318             # Delete the meeting
319             if record.case_id:
320                 obj_crm_meeting.unlink(cr, uid, [record.case_id.id])
321
322             # If a category that created several holidays, cancel all related
323             wf_service = netsvc.LocalService("workflow")
324             for request in record.linked_request_ids or []:
325                 wf_service.trg_validate(uid, 'hr.holidays', request.id, 'cancel', cr)
326
327         self._remove_resource_leave(cr, uid, ids, context=context)
328         return True
329
330     def check_holidays(self, cr, uid, ids, context=None):
331         holi_status_obj = self.pool.get('hr.holidays.status')
332         for record in self.browse(cr, uid, ids):
333             if record.holiday_type == 'employee' and record.type == 'remove':
334                 if record.employee_id and not record.holiday_status_id.limit:
335                     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']
336                     if leaves_rest < record.number_of_days_temp:
337                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for employee %s: too few remaining days (%s).') % (record.employee_id.name, leaves_rest))
338         return True
339 hr_holidays()
340
341 class resource_calendar_leaves(osv.osv):
342     _inherit = "resource.calendar.leaves"
343     _description = "Leave Detail"
344     _columns = {
345         'holiday_id': fields.many2one("hr.holidays", "Holiday"),
346     }
347
348 resource_calendar_leaves()
349
350
351 class hr_employee(osv.osv):
352    _inherit="hr.employee"
353
354    def _set_remaining_days(self, cr, uid, empl_id, name, value, arg, context=None):
355         employee = self.browse(cr, uid, empl_id, context=context)
356         diff = value - employee.remaining_leaves
357         type_obj = self.pool.get('hr.holidays.status')
358         holiday_obj = self.pool.get('hr.holidays')
359         # Find for holidays status
360         status_ids = type_obj.search(cr, uid, [('limit', '=', False)], context=context)
361         if len(status_ids) != 1 :
362             raise osv.except_osv(_('Warning !'),_("To use this feature, you must have only one leave type without the option 'Allow to Override Limit' set. (%s Found).") % (len(status_ids)))
363         status_id = status_ids and status_ids[0] or False
364         if not status_id:
365             return False
366         if diff > 0:
367             leave_id = holiday_obj.create(cr, uid, {'name': _('Allocation for %s') % employee.name, 'employee_id': employee.id, 'holiday_status_id': status_id, 'type': 'add', 'holiday_type': 'employee', 'number_of_days_temp': diff}, context=context)
368         elif diff < 0:
369             leave_id = holiday_obj.create(cr, uid, {'name': _('Leave Request for %s') % employee.name, 'employee_id': employee.id, 'holiday_status_id': status_id, 'type': 'remove', 'holiday_type': 'employee', 'number_of_days_temp': abs(diff)}, context=context)
370         else:
371             return False
372         wf_service = netsvc.LocalService("workflow")
373         wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'confirm', cr)
374         wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'validate', cr)
375         wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'second_validate', cr)
376         return True
377
378    def _get_remaining_days(self, cr, uid, ids, name, args, context=None):
379         cr.execute("""SELECT
380                 sum(h.number_of_days) as days,
381                 h.employee_id 
382             from
383                 hr_holidays h
384                 join hr_holidays_status s on (s.id=h.holiday_status_id) 
385             where
386                 h.state='validate' and
387                 s.limit=False and
388                 h.employee_id in (%s)
389             group by h.employee_id"""% (','.join(map(str,ids)),) )
390         res = cr.dictfetchall()
391         remaining = {}
392         for r in res:
393             remaining[r['employee_id']] = r['days']
394         for employee_id in ids:
395             if not remaining.get(employee_id):
396                 remaining[employee_id] = 0.0
397         return remaining
398
399    def _get_leave_status(self, cr, uid, ids, name, args, context=None):
400         holidays_id = self.pool.get('hr.holidays').search(cr, uid, 
401            [('employee_id', 'in', ids), ('date_from','<=',time.strftime('%Y-%m-%d %H:%M:%S')), 
402             ('date_to','>=',time.strftime('%Y-%m-%d %H:%M:%S')),('type','=','remove'),('state','not in',('cancel','refuse'))],
403            context=context)
404         result = {}
405         for id in ids:
406             result[id] = {
407                 'current_leave_state': False,
408                 'current_leave_id': False,
409             }
410         for holiday in self.pool.get('hr.holidays').browse(cr, uid, holidays_id, context=context):
411             result[holiday.employee_id.id]['current_leave_state'] = holiday.state
412             result[holiday.employee_id.id]['current_leave_id'] = holiday.holiday_status_id.id
413         return result
414
415    _columns = {
416         'remaining_leaves': fields.function(_get_remaining_days, string='Remaining Legal Leaves', fnct_inv=_set_remaining_days, type="float", help='Total number of legal leaves allocated to this employee, change this value to create allocation/leave requests.'),
417         'current_leave_state': fields.function(_get_leave_status, multi="leave_status", string="Current Leave Status", type="selection",
418             selection=[('draft', 'New'), ('confirm', 'Waiting Approval'), ('refuse', 'Refused'),
419             ('validate1', 'Waiting Second Approval'), ('validate', 'Approved'), ('cancel', 'Cancelled')]),
420         'current_leave_id': fields.function(_get_leave_status, multi="leave_status", string="Current Leave Type",type='many2one', relation='hr.holidays.status'),
421         'last_login': fields.related('user_id', 'date', type='datetime', string='Latest Connection', readonly=1)
422     }
423
424 hr_employee()
425
426 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: