[FIX] hr_timesheet_sheet : date_from and date_to problem, hr_evaluation : date deadli...
[odoo/odoo.git] / addons / hr_timesheet_sheet / hr_timesheet_sheet.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 import netsvc
24
25 from osv import fields, osv
26 from datetime import datetime
27 from dateutil.relativedelta import relativedelta
28 from tools.translate import _
29
30 class one2many_mod2(fields.one2many):
31     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
32         if context is None:
33             context = {}
34
35         if values is None:
36             values = {}
37
38         # dict:
39         # {idn: (date_current, user_id), ...
40         #  1: ('2010-08-15', 1)}
41         res6 = dict([(rec['id'], (rec['date_current'], rec['user_id'][0]))
42                         for rec
43                             in obj.read(cr, user, ids, ['date_current', 'user_id'], context=context)])
44
45         # eg: ['|', '|',
46         #       '&', '&', ('name', '>=', '2011-03-01'), ('name', '<=', '2011-03-01'), ('employee_id.user_id', '=', 1),
47         #       '&', '&', ('name', '>=', '2011-02-01'), ('name', '<=', '2011-02-01'), ('employee_id.user_id', '=', 1)]
48         dom = []
49         for c, id in enumerate(ids):
50             if id in res6:
51                 if c: # skip first
52                     dom.insert(0 ,'|')
53                 dom.append('&')
54                 dom.append('&')
55                 dom.append(('name', '>=', res6[id][0]))
56                 dom.append(('name', '<=', res6[id][0]))
57                 dom.append(('employee_id.user_id', '=', res6[id][1]))
58
59         ids2 = obj.pool.get(self._obj).search(cr, user, dom, limit=self._limit)
60
61         res = {}
62         for i in ids:
63             res[i] = []
64
65         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
66             if r[self._fields_id]:
67                 res[r[self._fields_id][0]].append(r['id'])
68
69         return res
70
71     def set(self, cr, obj, id, field, values, user=None, context=None):
72         if context is None:
73             context = {}
74
75         context = context.copy()
76         context['sheet_id'] = id
77         return super(one2many_mod2, self).set(cr, obj, id, field, values, user=user, context=context)
78
79
80 class one2many_mod(fields.one2many):
81     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
82         if context is None:
83             context = {}
84
85         if values is None:
86             values = {}
87
88
89         res5 = obj.read(cr, user, ids, ['date_current', 'user_id'], context=context)
90         res6 = {}
91         for r in res5:
92             res6[r['id']] = (r['date_current'], r['user_id'][0])
93
94         ids2 = []
95         for id in ids:
96             dom = []
97             if id in res6:
98                 dom = [('date', '=', res6[id][0]), ('user_id', '=', res6[id][1])]
99             ids2.extend(obj.pool.get(self._obj).search(cr, user,
100                 dom, limit=self._limit))
101         res = {}
102         for i in ids:
103             res[i] = []
104         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2,
105                 [self._fields_id], context=context, load='_classic_write'):
106             if r[self._fields_id]:
107                 res[r[self._fields_id][0]].append(r['id'])
108
109         return res
110
111 class hr_timesheet_sheet(osv.osv):
112     _name = "hr_timesheet_sheet.sheet"
113     _table = 'hr_timesheet_sheet_sheet'
114     _order = "id desc"
115     _description="Timesheet"
116
117     def _total_day(self, cr, uid, ids, name, args, context=None):
118         res = {}
119         if context is None:
120             context = {}
121         cr.execute('SELECT sheet.id, day.total_attendance, day.total_timesheet, day.total_difference\
122                 FROM hr_timesheet_sheet_sheet AS sheet \
123                 LEFT JOIN hr_timesheet_sheet_sheet_day AS day \
124                     ON (sheet.id = day.sheet_id \
125                         AND day.name = sheet.date_current) \
126                 WHERE sheet.id IN %s',(tuple(ids),))
127         for record in cr.fetchall():
128             res[record[0]] = {}
129             res[record[0]]['total_attendance_day'] = record[1]
130             res[record[0]]['total_timesheet_day'] = record[2]
131             res[record[0]]['total_difference_day'] = record[3]
132         return res
133
134     def _total(self, cr, uid, ids, name, args, context=None):
135         res = {}
136         if context is None:
137             context = {}
138         cr.execute('SELECT s.id, COALESCE(SUM(d.total_attendance),0), COALESCE(SUM(d.total_timesheet),0), COALESCE(SUM(d.total_difference),0) \
139                 FROM hr_timesheet_sheet_sheet s \
140                     LEFT JOIN hr_timesheet_sheet_sheet_day d \
141                         ON (s.id = d.sheet_id) \
142                 WHERE s.id IN %s GROUP BY s.id',(tuple(ids),))
143         for record in cr.fetchall():
144             res[record[0]] = {}
145             res[record[0]]['total_attendance'] = record[1]
146             res[record[0]]['total_timesheet'] = record[2]
147             res[record[0]]['total_difference'] = record[3]
148         return res
149
150     def _state_attendance(self, cr, uid, ids, name, args, context=None):
151         emp_obj = self.pool.get('hr.employee')
152         result = {}
153         link_emp = {}
154         emp_ids = []
155         if context is None:
156             context = {}
157
158         for sheet in self.browse(cr, uid, ids, context=context):
159             result[sheet.id] = 'none'
160             emp_ids2 = emp_obj.search(cr, uid,
161                     [('user_id', '=', sheet.user_id.id)], context=context)
162             if emp_ids2:
163                 link_emp[emp_ids2[0]] = sheet.id
164                 emp_ids.append(emp_ids2[0])
165         for emp in emp_obj.browse(cr, uid, emp_ids, context=context):
166             if emp.id in link_emp:
167                 sheet_id = link_emp[emp.id]
168                 result[sheet_id] = emp.state
169         return result
170
171     def check_employee_attendance_state(self, cr, uid, sheet_id, context=None):
172         ids_signin = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_in')])
173         ids_signout = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_out')])
174
175         if len(ids_signin) != len(ids_signout):
176             raise osv.except_osv(('Warning !'),_('The timesheet cannot be validated as it does not contain equal no. of sign ins and sign outs!'))
177         return True
178
179     def copy(self, cr, uid, ids, *args, **argv):
180         raise osv.except_osv(_('Error !'), _('You cannot duplicate a timesheet !'))
181
182     def create(self, cr, uid, vals, *args, **argv):
183         if 'employee_id' in vals:
184             if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).user_id:
185                 raise osv.except_osv(_('Error !'), _('You cannot create a timesheet for an employee that does not have any user defined !'))
186         return super(hr_timesheet_sheet, self).create(cr, uid, vals, *args, **argv)
187
188     def write(self, cr, uid, ids, vals, *args, **argv):
189         if 'employee_id' in vals:
190             new_user_id = self.pool.get('hr.employee').browse(cr, uid, vals['employee_id']).user_id.id or False
191             if not new_user_id:
192                 raise osv.except_osv(_('Error !'), _('You cannot create a timesheet for an employee that does not have any user defined !'))
193             if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id):
194                 raise osv.except_osv(_('Error !'), _('You can not have 2 timesheets that overlaps !\nPlease use the menu \'My Current Timesheet\' to avoid this problem.'))
195         return super(hr_timesheet_sheet, self).write(cr, uid, ids, vals, *args, **argv)
196
197     def button_confirm(self, cr, uid, ids, context=None):
198         if context is None:
199             context = {}
200         for sheet in self.browse(cr, uid, ids, context=context):
201             self.check_employee_attendance_state(cr, uid, sheet.id, context)
202             di = sheet.user_id.company_id.timesheet_max_difference
203             if (abs(sheet.total_difference) < di) or not di:
204                 wf_service = netsvc.LocalService("workflow")
205                 wf_service.trg_validate(uid, 'hr_timesheet_sheet.sheet', sheet.id, 'confirm', cr)
206             else:
207                 raise osv.except_osv(_('Warning !'), _('Please verify that the total difference of the sheet is lower than %.2f !') %(di,))
208         return True
209
210     def date_today(self, cr, uid, ids, context=None):
211         if context is None:
212             context = {}
213         for sheet in self.browse(cr, uid, ids, context=context):
214             if datetime.today() <= datetime.strptime(sheet.date_from, '%Y-%m-%d'):
215                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context)
216             elif datetime.now() >= datetime.strptime(sheet.date_to, '%Y-%m-%d'):
217                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context)
218             else:
219                 self.write(cr, uid, [sheet.id], {'date_current': time.strftime('%Y-%m-%d')}, context=context)
220         return True
221
222     def date_previous(self, cr, uid, ids, context=None):
223         if context is None:
224             context = {}
225         for sheet in self.browse(cr, uid, ids, context=context):
226             if datetime.strptime(sheet.date_current, '%Y-%m-%d') <= datetime.strptime(sheet.date_from, '%Y-%m-%d'):
227                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context)
228             else:
229                 self.write(cr, uid, [sheet.id], {
230                     'date_current': (datetime.strptime(sheet.date_current, '%Y-%m-%d') + relativedelta(days=-1)).strftime('%Y-%m-%d'),
231                 }, context=context)
232         return True
233
234     def date_next(self, cr, uid, ids, context=None):
235         if context is None:
236             context = {}
237         for sheet in self.browse(cr, uid, ids, context=context):
238             if datetime.strptime(sheet.date_current, '%Y-%m-%d') >= datetime.strptime(sheet.date_to, '%Y-%m-%d'):
239                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context)
240             else:
241                 self.write(cr, uid, [sheet.id], {
242                     'date_current': (datetime.strptime(sheet.date_current, '%Y-%m-%d') + relativedelta(days=1)).strftime('%Y-%m-%d'),
243                 }, context=context)
244         return True
245
246     def button_dummy(self, cr, uid, ids, context=None):
247         if context is None:
248             context = {}
249         for sheet in self.browse(cr, uid, ids, context=context):
250             if datetime.strptime(sheet.date_current, '%Y-%m-%d') <= datetime.strptime(sheet.date_from, '%Y-%m-%d'):
251                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,}, context=context)
252             elif datetime.strptime(sheet.date_current, '%Y-%m-%d') >= datetime.strptime(sheet.date_to, '%Y-%m-%d'):
253                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,}, context=context)
254         return True
255
256     def sign_in(self, cr, uid, ids, context=None):
257         emp_obj = self.pool.get('hr.employee')
258         if context is None:
259             context = {}
260         if not self.browse(cr, uid, ids, context=context)[0].date_current == time.strftime('%Y-%m-%d'):
261             raise osv.except_osv(_('Error !'), _('You can not sign in from an other date than today'))
262         emp_ids = emp_obj.search(cr, uid, [('user_id', '=', uid)], context=context)
263         context['sheet_id']=ids[0]
264         success = emp_obj.attendance_action_change(cr, uid, emp_ids, type='sign_in', context=context,)
265         return True
266
267     def sign_out(self, cr, uid, ids, context=None):
268         emp_obj = self.pool.get('hr.employee')
269         if context is None:
270             context = {}
271         if not self.browse(cr, uid, ids, context=context)[0].date_current == time.strftime('%Y-%m-%d'):
272             raise osv.except_osv(_('Error !'), _('You can not sign out from an other date than today'))
273         emp_ids = emp_obj.search(cr, uid, [('user_id', '=', uid)])
274         context['sheet_id']=ids[0]
275         success = emp_obj.attendance_action_change(cr, uid, emp_ids, type='sign_out', context=context,)
276         return True
277
278     _columns = {
279         'name': fields.char('Description', size=64, select=1,
280                             states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
281         'employee_id': fields.many2one('hr.employee', 'Employee', required=True),
282         'user_id': fields.related('employee_id', 'user_id', type="many2one", relation="res.users", store=True, string="User", required=False, readonly=True),#fields.many2one('res.users', 'User', required=True, select=1, states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
283         'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
284         'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
285         'date_current': fields.date('Current date', required=True),
286         'timesheet_ids' : one2many_mod('hr.analytic.timesheet', 'sheet_id',
287             'Timesheet lines', domain=[('date', '=', time.strftime('%Y-%m-%d'))],
288             readonly=True, states={
289                 'draft': [('readonly', False)],
290                 'new': [('readonly', False)]}
291             ),
292         'attendances_ids' : one2many_mod2('hr.attendance', 'sheet_id', 'Attendances', readonly=True, states={'draft':[('readonly',False)],'new':[('readonly',False)]}),
293         'state' : fields.selection([
294             ('new', 'New'),
295             ('draft','Draft'),
296             ('confirm','Confirmed'),
297             ('done','Done')], 'State', select=True, required=True, readonly=True,
298             help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed timesheet. \
299                 \n* The \'Confirmed\' state is used for to confirm the timesheet by user. \
300                 \n* The \'Done\' state is used when users timesheet is accepted by his/her senior.'),
301         'state_attendance' : fields.function(_state_attendance, method=True, type='selection', selection=[('absent', 'Absent'), ('present', 'Present'),('none','No employee defined')], string='Current Status'),
302         'total_attendance_day': fields.function(_total_day, method=True, string='Total Attendance', multi="_total_day"),
303         'total_timesheet_day': fields.function(_total_day, method=True, string='Total Timesheet', multi="_total_day"),
304         'total_difference_day': fields.function(_total_day, method=True, string='Difference', multi="_total_day"),
305         'total_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total_sheet"),
306         'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', multi="_total_sheet"),
307         'total_difference': fields.function(_total, method=True, string='Difference', multi="_total_sheet"),
308         'period_ids': fields.one2many('hr_timesheet_sheet.sheet.day', 'sheet_id', 'Period', readonly=True),
309         'account_ids': fields.one2many('hr_timesheet_sheet.sheet.account', 'sheet_id', 'Analytic accounts', readonly=True),
310         'company_id': fields.many2one('res.company', 'Company'),
311         'department_id':fields.many2one('hr.department','Department'),
312     }
313
314     def _default_date_from(self,cr, uid, context=None):
315         if context is None:
316             context = {}
317         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
318         r = user.company_id and user.company_id.timesheet_range or 'month'
319         if r=='month':
320             return time.strftime('%Y-%m-01')
321         elif r=='week':
322             return (datetime.today() + relativedelta(weekday=0, weeks=-1)).strftime('%Y-%m-%d')
323         elif r=='year':
324             return time.strftime('%Y-01-01')
325         return time.strftime('%Y-%m-%d')
326
327     def _default_date_to(self,cr, uid, context=None):
328         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
329         r = user.company_id and user.company_id.timesheet_range or 'month'
330         if r=='month':
331             return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
332         elif r=='week':
333             return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
334         elif r=='year':
335             return time.strftime('%Y-12-31')
336         return time.strftime('%Y-%m-%d')
337
338     def _default_employee(self,cr, uid, context=None):
339         emp_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
340         return emp_ids and emp_ids[0] or False
341
342     _defaults = {
343         'date_from' : _default_date_from,
344         'date_current' : lambda *a: time.strftime('%Y-%m-%d'),
345         'date_to' : _default_date_to,
346         'state': 'new',
347         'employee_id': _default_employee,
348         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr_timesheet_sheet.sheet', context=c)
349     }
350
351     def _sheet_date(self, cr, uid, ids, forced_user_id=False):
352         for sheet in self.browse(cr, uid, ids):
353             new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id
354             if new_user_id:
355                 cr.execute('SELECT id \
356                     FROM hr_timesheet_sheet_sheet \
357                     WHERE (date_from < %s and %s < date_to) \
358                         AND user_id=%s \
359                         AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
360                 if cr.fetchall():
361                     return False
362         return True
363
364     def _date_current_check(self, cr, uid, ids):
365         for sheet in self.browse(cr, uid, ids):
366             if sheet.date_current < sheet.date_from or sheet.date_current > sheet.date_to:
367                 return False
368         return True
369
370
371     _constraints = [
372         (_sheet_date, 'You can not have 2 timesheets that overlaps !\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']),
373         (_date_current_check, 'You must select a Current date which is in the timesheet dates !', ['date_current']),
374     ]
375
376     def action_set_to_draft(self, cr, uid, ids, *args):
377         self.write(cr, uid, ids, {'state': 'draft'})
378         wf_service = netsvc.LocalService('workflow')
379         for id in ids:
380             wf_service.trg_create(uid, self._name, id, cr)
381         return True
382
383     def name_get(self, cr, uid, ids, context=None):
384         if context is None:
385             context = {}
386         if not len(ids):
387             return []
388         return [(r['id'], r['date_from'] + ' - ' + r['date_to']) \
389                 for r in self.read(cr, uid, ids, ['date_from', 'date_to'],
390                     context=context, load='_classic_write')]
391
392     def unlink(self, cr, uid, ids, context=None):
393         if context is None:
394             context = {}
395         sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
396         for sheet in sheets:
397             if sheet['state'] in ('confirm', 'done'):
398                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sheet(s) which are already confirmed !'))
399             elif sheet['total_attendance'] <> 0.00:
400                 raise osv.except_osv(_('Invalid action !'), _('Cannot delete Sheet(s) which have attendance entries encoded !'))
401         return super(hr_timesheet_sheet, self).unlink(cr, uid, ids, context=context)
402
403 hr_timesheet_sheet()
404
405
406 class hr_timesheet_line(osv.osv):
407     _inherit = "hr.analytic.timesheet"
408
409     def _get_default_date(self, cr, uid, context=None):
410         if context is None:
411             context = {}
412         if 'date' in context:
413             return context['date']
414         return time.strftime('%Y-%m-%d')
415
416     def _sheet(self, cursor, user, ids, name, args, context=None):
417         sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
418         if context is None:
419             context = {}
420         cursor.execute('SELECT l.id, COALESCE(MAX(s.id), 0) \
421                 FROM hr_timesheet_sheet_sheet s \
422                     LEFT JOIN (hr_analytic_timesheet l \
423                         LEFT JOIN account_analytic_line al \
424                             ON (l.line_id = al.id)) \
425                         ON (s.date_to >= al.date \
426                             AND s.date_from <= al.date \
427                             AND s.user_id = al.user_id) \
428                 WHERE l.id IN %s GROUP BY l.id',(tuple(ids),))
429         res = dict(cursor.fetchall())
430         sheet_names = {}
431         for sheet_id, name in sheet_obj.name_get(cursor, user, res.values(),
432                 context=context):
433             sheet_names[sheet_id] = name
434
435         for line_id in {}.fromkeys(ids):
436             sheet_id = res.get(line_id, False)
437             if sheet_id:
438                 res[line_id] = (sheet_id, sheet_names[sheet_id])
439             else:
440                 res[line_id] = False
441         return res
442
443     def _sheet_search(self, cursor, user, obj, name, args, context=None):
444         if context is None:
445             context = {}
446         if not len(args):
447             return []
448         sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
449
450         i = 0
451         while i < len(args):
452             fargs = args[i][0].split('.', 1)
453             if len(fargs) > 1:
454                 args[i] = (fargs[0], 'in', sheet_obj.search(cursor, user,
455                     [(fargs[1], args[i][1], args[i][2])], context=context))
456                 i += 1
457                 continue
458             if isinstance(args[i][2], basestring):
459                 res_ids = sheet_obj.name_search(cursor, user, args[i][2], [],
460                         args[i][1])
461                 args[i] = (args[i][0], 'in', [x[0] for x in res_ids])
462             i += 1
463         qu1, qu2 = [], []
464         for x in args:
465             if x[1] != 'in':
466                 if (x[2] is False) and (x[1] == '='):
467                     qu1.append('(s.id IS NULL)')
468                 elif (x[2] is False) and (x[1] == '<>' or x[1] == '!='):
469                     qu1.append('(s.id IS NOT NULL)')
470                 else:
471                     qu1.append('(s.id %s %s)' % (x[1], '%s'))
472                     qu2.append(x[2])
473             elif x[1] == 'in':
474                 if len(x[2]) > 0:
475                     qu1.append('(s.id in (%s))' % (','.join(['%d'] * len(x[2]))))
476                     qu2 += x[2]
477                 else:
478                     qu1.append('(False)')
479         if len(qu1):
480             qu1 = ' WHERE ' + ' AND '.join(qu1)
481         else:
482             qu1 = ''
483         cursor.execute('SELECT l.id \
484                 FROM hr_timesheet_sheet_sheet s \
485                     LEFT JOIN (hr_analytic_timesheet l \
486                         LEFT JOIN account_analytic_line al \
487                             ON (l.line_id = al.id)) \
488                         ON (s.date_to >= al.date \
489                             AND s.date_from <= al.date \
490                             AND s.user_id = al.user_id)' + \
491                 qu1, qu2)
492         res = cursor.fetchall()
493         if not len(res):
494             return [('id', '=', '0')]
495         return [('id', 'in', [x[0] for x in res])]
496
497     _columns = {
498         'sheet_id': fields.function(_sheet, method=True, string='Sheet',
499             type='many2one', relation='hr_timesheet_sheet.sheet',
500             fnct_search=_sheet_search),
501     }
502     _defaults = {
503         'date': _get_default_date,
504     }
505
506     def create(self, cr, uid, vals, *args, **kwargs):
507         if vals.get('sheet_id', False):
508             ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, vals['sheet_id'])
509             if not ts.state in ('draft', 'new'):
510                 raise osv.except_osv(_('Error !'), _('You can not modify an entry in a confirmed timesheet !'))
511         return super(hr_timesheet_line,self).create(cr, uid, vals, *args, **kwargs)
512
513     def unlink(self, cr, uid, ids, *args, **kwargs):
514         self._check(cr, uid, ids)
515         return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
516
517     def write(self, cr, uid, ids, *args, **kwargs):
518         self._check(cr, uid, ids)
519         return super(hr_timesheet_line,self).write(cr, uid, ids,*args, **kwargs)
520
521     def _check(self, cr, uid, ids):
522         for att in self.browse(cr, uid, ids):
523             if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
524                 raise osv.except_osv(_('Error !'), _('You can not modify an entry in a confirmed timesheet !'))
525         return True
526
527 hr_timesheet_line()
528
529 class hr_attendance(osv.osv):
530     _inherit = "hr.attendance"
531
532     def _get_default_date(self, cr, uid, context=None):
533         if context is None:
534             context = {}
535         if 'name' in context:
536             return context['name'] + time.strftime(' %H:%M:%S')
537         return time.strftime('%Y-%m-%d %H:%M:%S')
538
539     def _sheet(self, cursor, user, ids, name, args, context=None):
540         if context is None:
541             context = {}
542         sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
543         cursor.execute("SELECT a.id, COALESCE(MAX(s.id), 0) \
544                 FROM hr_timesheet_sheet_sheet s \
545                     LEFT JOIN (hr_attendance a \
546                         LEFT JOIN hr_employee e \
547                             LEFT JOIN resource_resource r \
548                                 ON (e.resource_id = r.id) \
549                             ON (a.employee_id = e.id)) \
550                         ON (s.date_to >= date_trunc('day',a.name) \
551                             AND s.date_from <= a.name \
552                             AND s.user_id = r.user_id) \
553                 WHERE a.id IN %s GROUP BY a.id",(tuple(ids),))
554         res = dict(cursor.fetchall())
555         sheet_names = {}
556         for sheet_id, name in sheet_obj.name_get(cursor, user, res.values(),
557                 context=context):
558             sheet_names[sheet_id] = name
559         for line_id in {}.fromkeys(ids):
560             sheet_id = res.get(line_id, False)
561             if sheet_id:
562                 res[line_id] = (sheet_id, sheet_names[sheet_id])
563             else:
564                 res[line_id] = False
565         return res
566
567     def _sheet_search(self, cursor, user, obj, name, args, context=None):
568         if not len(args):
569             return []
570
571         sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
572         if context is None:
573             context = {}
574         i = 0
575         while i < len(args):
576             fargs = args[i][0].split('.', 1)
577             if len(fargs) > 1:
578                 args[i] = (fargs[0], 'in', sheet_obj.search(cursor, user,
579                     [(fargs[1], args[i][1], args[i][2])], context=context))
580                 i += 1
581                 continue
582             if isinstance(args[i][2], basestring):
583                 res_ids = sheet_obj.name_search(cursor, user, args[i][2], [],
584                         args[i][1])
585                 args[i] = (args[i][0], 'in', [x[0] for x in res_ids])
586             i += 1
587         qu1, qu2 = [], []
588         for x in args:
589             if x[1] != 'in':
590                 if (x[2] is False) and (x[1] == '='):
591                     qu1.append('(s.id IS NULL)')
592                 elif (x[2] is False) and (x[1] == '<>' or x[1] == '!='):
593                     qu1.append('(s.id IS NOT NULL)')
594                 else:
595                     qu1.append('(s.id %s %s)' % (x[1], '%s'))
596                     qu2.append(x[2])
597             elif x[1] == 'in':
598                 if len(x[2]) > 0:
599                     qu1.append('(s.id in (%s))' % (','.join(['%d'] * len(x[2]))))
600                     qu2 += x[2]
601                 else:
602                     qu1.append('(False)')
603         if len(qu1):
604             qu1 = ' WHERE ' + ' AND '.join(qu1)
605         else:
606             qu1 = ''
607         cursor.execute('SELECT a.id\
608                 FROM hr_timesheet_sheet_sheet s \
609                     LEFT JOIN (hr_attendance a \
610                         LEFT JOIN hr_employee e \
611                             ON (a.employee_id = e.id)) \
612                                 LEFT JOIN resource_resource r \
613                                     ON (e.resource_id = r.id) \
614                         ON (s.date_to >= date_trunc(\'day\',a.name) \
615                             AND s.date_from <= a.name \
616                             AND s.user_id = r.user_id) ' + \
617                 qu1, qu2)
618         res = cursor.fetchall()
619         if not len(res):
620             return [('id', '=', '0')]
621         return [('id', 'in', [x[0] for x in res])]
622
623     _columns = {
624         'sheet_id': fields.function(_sheet, method=True, string='Sheet',
625             type='many2one', relation='hr_timesheet_sheet.sheet',
626             fnct_search=_sheet_search),
627     }
628     _defaults = {
629         'name': _get_default_date,
630     }
631
632     def create(self, cr, uid, vals, context=None):
633         if context is None:
634             context = {}
635         if 'sheet_id' in context:
636             ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, context['sheet_id'], context=context)
637             if ts.state not in ('draft', 'new'):
638                 raise osv.except_osv(_('Error !'), _('You cannot modify an entry in a confirmed timesheet !'))
639         res = super(hr_attendance,self).create(cr, uid, vals, context=context)
640         if 'sheet_id' in context:
641             if context['sheet_id'] != self.browse(cr, uid, res, context=context).sheet_id.id:
642                 raise osv.except_osv(_('UserError'), _('You cannot enter an attendance ' \
643                         'date outside the current timesheet dates!'))
644         return res
645
646     def unlink(self, cr, uid, ids, *args, **kwargs):
647         self._check(cr, uid, ids)
648         return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
649
650     def write(self, cr, uid, ids, vals, context=None):
651         if context is None:
652             context = {}
653         self._check(cr, uid, ids)
654         res = super(hr_attendance,self).write(cr, uid, ids, vals, context=context)
655         if 'sheet_id' in context:
656             for attendance in self.browse(cr, uid, ids, context=context):
657                 if context['sheet_id'] != attendance.sheet_id.id:
658                     raise osv.except_osv(_('UserError'), _('You cannot enter an attendance ' \
659                             'date outside the current timesheet dates!'))
660         return res
661
662     def _check(self, cr, uid, ids):
663         for att in self.browse(cr, uid, ids):
664             if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
665                 raise osv.except_osv(_('Error !'), _('You cannot modify an entry in a confirmed timesheet !'))
666         return True
667
668 hr_attendance()
669
670 class hr_timesheet_sheet_sheet_day(osv.osv):
671     _name = "hr_timesheet_sheet.sheet.day"
672     _description = "Timesheets by Period"
673     _auto = False
674     _order='name'
675     _columns = {
676         'name': fields.date('Date', readonly=True),
677         'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, select="1"),
678         'total_timesheet': fields.float('Project Timesheet', readonly=True),
679         'total_attendance': fields.float('Attendance', readonly=True),
680         'total_difference': fields.float('Difference', readonly=True),
681     }
682
683     def init(self, cr):
684         cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
685             SELECT
686                 id,
687                 name,
688                 sheet_id,
689                 total_timesheet,
690                 total_attendance,
691                 cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
692             FROM
693                 ((
694                     SELECT
695                         MAX(id) as id,
696                         name,
697                         sheet_id,
698                         SUM(total_timesheet) as total_timesheet,
699                         CASE WHEN SUM(total_attendance) < 0
700                             THEN (SUM(total_attendance) +
701                                 CASE WHEN current_date <> name
702                                     THEN 1440
703                                     ELSE (EXTRACT(hour FROM current_time) * 60) + EXTRACT(minute FROM current_time)
704                                 END
705                                 )
706                             ELSE SUM(total_attendance)
707                         END /60  as total_attendance
708                     FROM
709                         ((
710                             select
711                                 min(hrt.id) as id,
712                                 l.date::date as name,
713                                 s.id as sheet_id,
714                                 sum(l.unit_amount) as total_timesheet,
715                                 0.0 as total_attendance
716                             from
717                                 hr_analytic_timesheet hrt
718                                 left join (account_analytic_line l
719                                     LEFT JOIN hr_timesheet_sheet_sheet s
720                                     ON (s.date_to >= l.date
721                                         AND s.date_from <= l.date
722                                         AND s.user_id = l.user_id))
723                                     on (l.id = hrt.line_id)
724                             group by l.date::date, s.id
725                         ) union (
726                             select
727                                 -min(a.id) as id,
728                                 a.name::date as name,
729                                 s.id as sheet_id,
730                                 0.0 as total_timesheet,
731                                 SUM(((EXTRACT(hour FROM a.name) * 60) + EXTRACT(minute FROM a.name)) * (CASE WHEN a.action = 'sign_in' THEN -1 ELSE 1 END)) as total_attendance
732                             from
733                                 hr_attendance a
734                                 LEFT JOIN (hr_timesheet_sheet_sheet s
735                                     LEFT JOIN resource_resource r
736                                         LEFT JOIN hr_employee e
737                                         ON (e.resource_id = r.id)
738                                     ON (s.user_id = r.user_id))
739                                 ON (a.employee_id = e.id
740                                     AND s.date_to >= date_trunc('day',a.name)
741                                     AND s.date_from <= a.name)
742                             WHERE action in ('sign_in', 'sign_out')
743                             group by a.name::date, s.id
744                         )) AS foo
745                         GROUP BY name, sheet_id
746                 )) AS bar""")
747
748 hr_timesheet_sheet_sheet_day()
749
750
751 class hr_timesheet_sheet_sheet_account(osv.osv):
752     _name = "hr_timesheet_sheet.sheet.account"
753     _description = "Timesheets by Period"
754     _auto = False
755     _order='name'
756     _columns = {
757         'name': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
758         'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True),
759         'total': fields.float('Total Time', digits=(16,2), readonly=True),
760         'invoice_rate': fields.many2one('hr_timesheet_invoice.factor', 'Invoice rate', readonly=True),
761         }
762
763     def init(self, cr):
764         cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
765             select
766                 min(hrt.id) as id,
767                 l.account_id as name,
768                 s.id as sheet_id,
769                 sum(l.unit_amount) as total,
770                 l.to_invoice as invoice_rate
771             from
772                 hr_analytic_timesheet hrt
773                 left join (account_analytic_line l
774                     LEFT JOIN hr_timesheet_sheet_sheet s
775                         ON (s.date_to >= l.date
776                             AND s.date_from <= l.date
777                             AND s.user_id = l.user_id))
778                     on (l.id = hrt.line_id)
779             group by l.account_id, s.id, l.to_invoice
780         )""")
781
782 hr_timesheet_sheet_sheet_account()
783
784
785
786 class res_company(osv.osv):
787     _inherit = 'res.company'
788     _columns = {
789         'timesheet_range': fields.selection(
790             [('day','Day'),('week','Week'),('month','Month'),('year','Year')], 'Timesheet range'),
791         'timesheet_max_difference': fields.float('Timesheet allowed difference(Hours)',
792             help="Allowed difference in hours between the sign in/out and the timesheet " \
793                  "computation for one sheet. Set this to 0 if you do not want any control."),
794     }
795     _defaults = {
796         'timesheet_range': lambda *args: 'week',
797         'timesheet_max_difference': lambda *args: 0.0
798     }
799
800 res_company()
801
802 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
803