[IMP]:mrp:Improved Cost Structure with set title.
[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 = [ lr.id for lr in record.linked_request_ids]
244                     self.holidays_cancel(cr, uid, list_ids)
245                     self.unlink(cr, uid, list_ids)
246
247     def _check_date(self, cr, uid, ids):
248         for rec in self.read(cr, uid, ids, ['number_of_days_temp', 'date_from','date_to', 'type']):
249             if rec['number_of_days_temp'] < 0:
250                 return False
251             if rec['type'] == 'add':
252                 continue
253             date_from = time.strptime(rec['date_from'], '%Y-%m-%d %H:%M:%S')
254             date_to = time.strptime(rec['date_to'], '%Y-%m-%d %H:%M:%S')
255             if date_from > date_to:
256                 return False
257         return True
258
259     _constraints = [(_check_date, 'Start date should not be larger than end date!\nNumber of Days should be greater than 1!', ['number_of_days_temp'])]
260
261     def unlink(self, cr, uid, ids, context=None):
262         if context is None:
263             context = {}
264         self._update_user_holidays(cr, uid, ids)
265         self._remove_resouce_leave(cr, uid, ids, context=context)
266         return super(hr_holidays, self).unlink(cr, uid, ids, context)
267
268     def onchange_date_from(self, cr, uid, ids, date_to, date_from):
269         result = {}
270         if date_to and date_from:
271             diff_day = self._get_number_of_days(date_from, date_to)
272             result['value'] = {
273                 'number_of_days_temp': round(diff_day)+1
274             }
275             return result
276         result['value'] = {
277             'number_of_days_temp': 0,
278         }
279         return result
280
281     def onchange_date_to(self, cr, uid, ids, date_from, date_to):
282         return self.onchange_date_from(cr, uid, ids, date_to, date_from)
283
284     def onchange_sec_id(self, cr, uid, ids, status, context=None):
285         if context is None:
286             context = {}
287         warning = {}
288         if status:
289             brows_obj = self.pool.get('hr.holidays.status').browse(cr, uid, [status], context=context)[0]
290             if brows_obj.categ_id and brows_obj.categ_id.section_id and not brows_obj.categ_id.section_id.allow_unlink:
291                 warning = {
292                     'title': "Warning for ",
293                     'message': "You won\'t be able to cancel this leave request because the CRM Sales Team of the leave type disallows."
294                         }
295         return {'warning': warning}
296
297     def set_to_draft(self, cr, uid, ids, *args):
298         wf_service = netsvc.LocalService("workflow")
299         self.write(cr, uid, ids, {
300             'state': 'draft',
301             'manager_id': False,
302             'number_of_days': 0,
303         })
304         for id in ids:
305             wf_service.trg_create(uid, 'hr.holidays', id, cr)
306         return True
307
308     def holidays_validate2(self, cr, uid, ids, *args):
309         vals = {'state':'validate1'}
310         self.check_holidays(cr, uid, ids)
311         ids2 = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)])
312         if ids2:
313             vals['manager_id'] = ids2[0]
314         else:
315             raise osv.except_osv(_('Warning !'),_('No user related to the selected employee.'))
316         self.write(cr, uid, ids, vals)
317         return True
318
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)])
325         if ids2:
326             if data_holiday[0].state == 'validate1':
327                 vals['manager_id2'] = ids2[0]
328             else:
329                 vals['manager_id'] = ids2[0]
330         else:
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':
335                 vals = {
336                    'name': record.name,
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
343                      }
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):
348                     vals = {
349                        'name': record.name,
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
356                          }
357                     self._create_resource_leave(cr, uid, vals)
358         return True
359
360     def holidays_confirm(self, cr, uid, ids, *args):
361         for record in self.browse(cr, uid, ids):
362             user_id = False
363             leave_asked = record.number_of_days_temp
364             if record.holiday_type == 'employee' and record.type == 'remove':
365                 if record.employee_id and not record.holiday_status_id.limit:
366                     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']
367                     if leaves_rest < leave_asked:
368                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for %s while available leaves are less than asked leaves.' %(record.employee_id.name)))
369                 nb = -(record.number_of_days_temp)
370             elif record.holiday_type == 'category' and record.type == 'remove':
371                 if record.category_id and not record.holiday_status_id.limit:
372                     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']
373                     if leaves_rest < leave_asked:
374                         raise osv.except_osv(_('Warning!'),_('You cannot validate leaves for %s while available leaves are less than asked leaves.' %(record.category_id.name)))
375                 nb = -(record.number_of_days_temp)
376             else:
377                 nb = record.number_of_days_temp
378
379             if record.holiday_type == 'employee' and record.employee_id:
380                 user_id = record.employee_id.user_id and record.employee_id.user_id.id or uid
381
382             self.write(cr, uid, [record.id], {'state':'confirm', 'number_of_days': nb, 'user_id': user_id })
383         return True
384
385     def holidays_refuse(self, cr, uid, ids, *args):
386         vals = {'state': 'refuse'}
387         ids2 = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)])
388         if ids2:
389             vals['manager_id'] = ids2[0]
390         self.write(cr, uid, ids, vals)
391         return True
392
393     def holidays_cancel(self, cr, uid, ids, *args):
394         self._update_user_holidays(cr, uid, ids)
395         self.write(cr, uid, ids, {'state': 'cancel'})
396         self._remove_resouce_leave(cr, uid, ids)
397         return True
398
399     def holidays_draft(self, cr, uid, ids, *args):
400         self.write(cr, uid, ids, {'state': 'draft'})
401         return True
402
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!!
425                 holiday_ids = []
426                 vals = {
427                     'name' : record.name,
428                     'holiday_status_id' : record.holiday_status_id.id,
429                     'state': 'draft',
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,
435                     'type': record.type,
436                     'allocation_type': record.allocation_type,
437                     'parent_id': record.id,
438                 }
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)])
443                     if user_id:
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)
448
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)
451                 vals = {
452                     'name' : record.name,
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,
458                 }
459                 case_id = meeting_obj.create(cr, uid, vals)
460                 self.write(cr, uid, ids, {'case_id': case_id})
461
462         return True
463
464 hr_holidays()
465
466 class resource_calendar_leaves(osv.osv):
467     _inherit = "resource.calendar.leaves"
468     _description = "Leave Detail"
469     _columns = {
470         'holiday_id': fields.many2one("hr.holidays", "Holiday"),
471     }
472
473 resource_calendar_leaves()
474
475 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: