1 # -*- coding: utf-8 -*-
2 ##################################################################################
4 # Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
5 # and 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # $Id: hr.py 4656 2006-11-24 09:58:42Z Cyp $
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.
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.
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/>.
22 ##############################################################################
26 from itertools import groupby
27 from operator import itemgetter
30 from osv import fields, osv
31 from tools.translate import _
34 class hr_holidays_status(osv.osv):
35 _name = "hr.holidays.status"
36 _description = "Leave Type"
38 def get_days_cat(self, cr, uid, ids, category_id, return_false, context=None):
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'])
46 grouped_lines = dict((k, [v for v in itr]) for k, itr in groupby(result, itemgetter('holiday_status_id')))
49 for record in self.browse(cr, uid, ids, context=context):
51 max_leaves = leaves_taken = 0
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'])
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
63 def get_days(self, cr, uid, ids, employee_id, return_false, context=None):
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'])
71 grouped_lines = dict((k, [v for v in itr]) for k, itr in groupby(result, itemgetter('holiday_status_id')))
74 for record in self.browse(cr, uid, ids, context=context):
76 max_leaves = leaves_taken = 0
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'])
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
88 def _user_left_days(self, cr, uid, ids, name, args, context=None):
95 if context and context.has_key('employee_id'):
96 if not context['employee_id']:
98 employee_id = context['employee_id']
100 employee_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
102 employee_id = employee_ids[0]
106 res = self.get_days(cr, uid, ids, employee_id, return_false, context=context)
108 res = dict.fromkeys(ids, {'leaves_taken': 0, 'remaining_leaves': 0, 'max_leaves': 0})
111 # To do: we can add remaining_leaves_category field to display remaining leaves for particular type
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")
130 class hr_holidays(osv.osv):
131 _name = "hr.holidays"
132 _description = "Leave"
133 _order = "type desc, date_from asc"
135 def _employee_get(obj, cr, uid, context=None):
138 ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context)
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)')
170 'employee_id': _employee_get,
173 'allocation_type': 'employee',
174 'user_id': lambda obj, cr, uid, context: uid,
175 'holiday_type': 'employee'
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 '''
182 obj_res_leave = self.pool.get('resource.calendar.leaves')
183 return obj_res_leave.create(cr, uid, vals, context=context)
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')
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)
193 def create(self, cr, uid, vals, context=None):
196 if 'holiday_type' in vals:
197 if vals['holiday_type'] == 'employee':
198 vals.update({'category_id': False})
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)
207 def write(self, cr, uid, ids, vals, context=None):
210 if 'holiday_type' in vals:
211 if vals['holiday_type'] == 'employee':
212 vals.update({'category_id': False})
214 vals.update({'employee_id': False})
215 return super(hr_holidays, self).write(cr, uid, ids, vals, context=context)
217 def onchange_type(self, cr, uid, ids, holiday_type):
219 if holiday_type == 'employee':
220 ids_employee = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)])
223 'employee_id': ids_employee[0]
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."""
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
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':
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)
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:
252 if rec['type'] == 'add':
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:
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'])]
262 def unlink(self, cr, uid, ids, context=None):
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)
269 def onchange_date_from(self, cr, uid, ids, date_to, date_from):
271 if date_to and date_from:
272 diff_day = self._get_number_of_days(date_from, date_to)
274 'number_of_days_temp': round(diff_day)+1
278 'number_of_days_temp': 0,
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)
285 def onchange_sec_id(self, cr, uid, ids, status, context=None):
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:
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."
296 return {'warning': warning}
298 def set_to_draft(self, cr, uid, ids, *args):
299 wf_service = netsvc.LocalService("workflow")
300 self.write(cr, uid, ids, {
306 wf_service.trg_create(uid, 'hr.holidays', id, cr)
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)])
314 vals['manager_id'] = ids2[0]
316 raise osv.except_osv(_('Warning !'),_('No user related to the selected employee.'))
317 return self.write(cr, uid, ids, vals)
319 def holidays_validate(self, cr, uid, ids, *args):
320 obj_emp = self.pool.get('hr.employee')
321 data_holiday = self.browse(cr, uid, ids)
322 self.check_holidays(cr, uid, ids)
323 vals = {'state':'validate'}
324 ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)])
326 if data_holiday[0].state == 'validate1':
327 vals['manager_id2'] = ids2[0]
329 vals['manager_id'] = ids2[0]
331 raise osv.except_osv(_('Warning !'), _('No user related to the selected employee.'))
332 self.write(cr, uid, ids, vals)
333 for record in data_holiday:
334 if record.holiday_type == 'employee' and record.type == 'remove':
337 'date_from': record.date_from,
338 'date_to': record.date_to,
339 'calendar_id': record.employee_id.calendar_id.id,
340 'company_id': record.employee_id.company_id.id,
341 'resource_id': record.employee_id.resource_id.id,
342 'holiday_id': record.id
344 self._create_resource_leave(cr, uid, vals)
345 elif record.holiday_type == 'category' and record.type == 'remove':
346 emp_ids = obj_emp.search(cr, uid, [('category_ids', '=', record.category_id.id)])
347 for emp in obj_emp.browse(cr, uid, emp_ids):
350 'date_from': record.date_from,
351 'date_to': record.date_to,
352 'calendar_id': emp.calendar_id.id,
353 'company_id': emp.company_id.id,
354 'resource_id': emp.resource_id.id,
355 'holiday_id':record.id
357 self._create_resource_leave(cr, uid, vals)
360 def holidays_confirm(self, cr, uid, ids, *args):
361 obj_hr_holiday_status = self.pool.get('hr.holidays.status')
362 for record in self.browse(cr, uid, ids):
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 = 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']
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)
378 nb = record.number_of_days_temp
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
383 self.write(cr, uid, [record.id], {'state':'confirm', 'number_of_days': nb, 'user_id': user_id })
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)])
390 vals['manager_id'] = ids2[0]
391 self.write(cr, uid, ids, vals)
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)
400 def holidays_draft(self, cr, uid, ids, *args):
401 return self.write(cr, uid, ids, {'state': 'draft'})
403 def check_holidays(self, cr, uid, ids):
404 holi_status_obj = self.pool.get('hr.holidays.status')
405 emp_obj = self.pool.get('hr.employee')
406 meeting_obj = self.pool.get('crm.meeting')
407 for record in self.browse(cr, uid, ids):
408 if not record.number_of_days:
409 raise osv.except_osv(_('Warning!'), _('Wrong leave definition.'))
410 if record.holiday_type=='employee' and record.employee_id:
411 leave_asked = record.number_of_days
412 if leave_asked < 0.00:
413 if not record.holiday_status_id.limit:
414 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']
415 if leaves_rest < -(leave_asked):
416 raise osv.except_osv(_('Warning!'),_('You Cannot Validate leaves while available leaves are less than asked leaves.'))
417 elif record.holiday_type == 'category' and record.category_id:
418 leave_asked = record.number_of_days
419 if leave_asked < 0.00:
420 if not record.holiday_status_id.limit:
421 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']
422 if leaves_rest < -(leave_asked):
423 raise osv.except_osv(_('Warning!'),_('You Cannot Validate leaves while available leaves are less than asked leaves.'))
424 else:# This condition will never meet!!
428 'holiday_status_id': record.holiday_status_id.id,
430 'date_from': record.date_from,
431 'date_to': record.date_to,
432 'notes': record.notes,
433 'number_of_days': record.number_of_days,
434 'number_of_days_temp': record.number_of_days_temp,
436 'allocation_type': record.allocation_type,
437 'parent_id': record.id,
439 employee_ids = emp_obj.search(cr, uid, [])
440 for employee in employee_ids:
441 vals['employee_id'] = employee
442 user_id = emp_obj.search(cr, uid, [('user_id','=',uid)])
444 vals['user_id'] = user_id[0]
445 holiday_ids.append(self.create(cr, uid, vals, context=None))
446 self.holidays_confirm(cr, uid, holiday_ids)
447 self.holidays_validate(cr, uid, holiday_ids)
449 if record.holiday_status_id.categ_id and record.date_from and record.date_to and record.employee_id:
450 diff_day = self._get_number_of_days(record.date_from, record.date_to)
453 'categ_id': record.holiday_status_id.categ_id.id,
454 'duration': (diff_day) * 8,
455 'note': record.notes,
456 'user_id': record.user_id.id,
457 'date': record.date_from,
459 case_id = meeting_obj.create(cr, uid, vals)
460 self.write(cr, uid, ids, {'case_id': case_id})
466 class resource_calendar_leaves(osv.osv):
467 _inherit = "resource.calendar.leaves"
468 _description = "Leave Detail"
470 'holiday_id': fields.many2one("hr.holidays", "Holiday"),
473 resource_calendar_leaves()
475 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: