[UPD] resolve conflicts with main branch and fix bug introduced in rev 10015 lp:1313622
[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 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
25 from pytz import timezone
26 import pytz
27
28 from openerp.osv import fields, osv
29 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
30 from openerp.tools.translate import _
31 from openerp import netsvc
32
33 class hr_timesheet_sheet(osv.osv):
34     _name = "hr_timesheet_sheet.sheet"
35     _inherit = "mail.thread"
36     _table = 'hr_timesheet_sheet_sheet'
37     _order = "id desc"
38     _description="Timesheet"
39
40     def _total(self, cr, uid, ids, name, args, context=None):
41         """ Compute the attendances, analytic lines timesheets and differences between them
42             for all the days of a timesheet and the current day
43         """
44
45         res = {}
46         for sheet in self.browse(cr, uid, ids, context=context or {}):
47             res.setdefault(sheet.id, {
48                 'total_attendance': 0.0,
49                 'total_timesheet': 0.0,
50                 'total_difference': 0.0,
51             })
52             for period in sheet.period_ids:
53                 res[sheet.id]['total_attendance'] += period.total_attendance
54                 res[sheet.id]['total_timesheet'] += period.total_timesheet
55                 res[sheet.id]['total_difference'] += period.total_attendance - period.total_timesheet
56         return res
57
58     def check_employee_attendance_state(self, cr, uid, sheet_id, context=None):
59         ids_signin = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_in')])
60         ids_signout = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_out')])
61
62         if len(ids_signin) != len(ids_signout):
63             raise osv.except_osv(('Warning!'),_('The timesheet cannot be validated as it does not contain an equal number of sign ins and sign outs.'))
64         return True
65
66     def copy(self, cr, uid, ids, *args, **argv):
67         raise osv.except_osv(_('Error!'), _('You cannot duplicate a timesheet.'))
68
69     def create(self, cr, uid, vals, context=None):
70         if 'employee_id' in vals:
71             if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).user_id:
72                 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must assign it to a user.'))
73             if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).product_id:
74                 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product, like \'Consultant\'.'))
75             if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).journal_id:
76                 raise osv.except_osv(_('Configuration Error!'), _('In order to create a timesheet for this employee, you must assign an analytic journal to the employee, like \'Timesheet Journal\'.'))
77         if vals.get('attendances_ids'):
78             # If attendances, we sort them by date asc before writing them, to satisfy the alternance constraint
79             vals['attendances_ids'] = self.sort_attendances(cr, uid, vals['attendances_ids'], context=context)
80         return super(hr_timesheet_sheet, self).create(cr, uid, vals, context=context)
81
82     def write(self, cr, uid, ids, vals, context=None):
83         if 'employee_id' in vals:
84             new_user_id = self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).user_id.id or False
85             if not new_user_id:
86                 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must assign it to a user.'))
87             if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id, context=context):
88                 raise osv.except_osv(_('Error!'), _('You cannot have 2 timesheets that overlap!\nYou should use the menu \'My Timesheet\' to avoid this problem.'))
89             if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).product_id:
90                 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product.'))
91             if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).journal_id:
92                 raise osv.except_osv(_('Configuration Error!'), _('In order to create a timesheet for this employee, you must assign an analytic journal to the employee, like \'Timesheet Journal\'.'))
93         if vals.get('attendances_ids'):
94             # If attendances, we sort them by date asc before writing them, to satisfy the alternance constraint
95             # In addition to the date order, deleting attendances are done before inserting attendances
96             vals['attendances_ids'] = self.sort_attendances(cr, uid, vals['attendances_ids'], context=context)
97         res = super(hr_timesheet_sheet, self).write(cr, uid, ids, vals, context=context)
98         if vals.get('attendances_ids'):
99             for timesheet in self.browse(cr, uid, ids):
100                 if not self.pool['hr.attendance']._altern_si_so(cr, uid, [att.id for att in timesheet.attendances_ids]):
101                     raise osv.except_osv(_('Warning !'), _('Error ! Sign in (resp. Sign out) must follow Sign out (resp. Sign in)'))
102         return res
103
104     def sort_attendances(self, cr, uid, attendance_tuples, context=None):
105         date_attendances = []
106         for att_tuple in attendance_tuples:
107             if att_tuple[0] in [0,1,4]:
108                 if att_tuple[0] in [0,1]:
109                     name = att_tuple[2]['name']
110                 else:
111                     name = self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name
112                 date_attendances.append((1, name, att_tuple))
113             elif att_tuple[0] in [2,3]:
114                 date_attendances.append((0, self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name, att_tuple))
115             else: 
116                 date_attendances.append((0, False, att_tuple))
117         date_attendances.sort()
118         return [att[2] for att in date_attendances]
119
120     def button_confirm(self, cr, uid, ids, context=None):
121         for sheet in self.browse(cr, uid, ids, context=context):
122             if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
123                 self.message_subscribe_users(cr, uid, [sheet.id], user_ids=[sheet.employee_id.parent_id.user_id.id], context=context)
124             self.check_employee_attendance_state(cr, uid, sheet.id, context=context)
125             di = sheet.user_id.company_id.timesheet_max_difference
126             if (abs(sheet.total_difference) < di) or not di:
127                 wf_service = netsvc.LocalService("workflow")
128                 wf_service.trg_validate(uid, 'hr_timesheet_sheet.sheet', sheet.id, 'confirm', cr)
129             else:
130                 raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
131         return True
132
133     def attendance_action_change(self, cr, uid, ids, context=None):
134         hr_employee = self.pool.get('hr.employee')
135         employee_ids = []
136         for sheet in self.browse(cr, uid, ids, context=context):
137             if sheet.employee_id.id not in employee_ids: employee_ids.append(sheet.employee_id.id)
138         return hr_employee.attendance_action_change(cr, uid, employee_ids, context=context)
139
140     _columns = {
141         'name': fields.char('Note', size=64, select=1,
142                             states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
143         'employee_id': fields.many2one('hr.employee', 'Employee', required=True),
144         '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)]}),
145         'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
146         'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
147         'timesheet_ids' : fields.one2many('hr.analytic.timesheet', 'sheet_id',
148             'Timesheet lines',
149             readonly=True, states={
150                 'draft': [('readonly', False)],
151                 'new': [('readonly', False)]}
152             ),
153         'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
154         'state' : fields.selection([
155             ('new', 'New'),
156             ('draft','Open'),
157             ('confirm','Waiting Approval'),
158             ('done','Approved')], 'Status', select=True, required=True, readonly=True,
159             help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed timesheet. \
160                 \n* The \'Confirmed\' status is used for to confirm the timesheet by user. \
161                 \n* The \'Done\' status is used when users timesheet is accepted by his/her senior.'),
162         'state_attendance' : fields.related('employee_id', 'state', type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Current Status', readonly=True),
163         'total_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total"),
164         'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', multi="_total"),
165         'total_difference': fields.function(_total, method=True, string='Difference', multi="_total"),
166         'period_ids': fields.one2many('hr_timesheet_sheet.sheet.day', 'sheet_id', 'Period', readonly=True),
167         'account_ids': fields.one2many('hr_timesheet_sheet.sheet.account', 'sheet_id', 'Analytic accounts', readonly=True),
168         'company_id': fields.many2one('res.company', 'Company'),
169         'department_id':fields.many2one('hr.department','Department'),
170     }
171
172     def _default_date_from(self, cr, uid, context=None):
173         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
174         r = user.company_id and user.company_id.timesheet_range or 'month'
175         if r=='month':
176             return time.strftime('%Y-%m-01')
177         elif r=='week':
178             return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
179         elif r=='year':
180             return time.strftime('%Y-01-01')
181         return time.strftime('%Y-%m-%d')
182
183     def _default_date_to(self, cr, uid, context=None):
184         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
185         r = user.company_id and user.company_id.timesheet_range or 'month'
186         if r=='month':
187             return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
188         elif r=='week':
189             return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
190         elif r=='year':
191             return time.strftime('%Y-12-31')
192         return time.strftime('%Y-%m-%d')
193
194     def _default_employee(self, cr, uid, context=None):
195         emp_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
196         return emp_ids and emp_ids[0] or False
197
198     _defaults = {
199         'date_from' : _default_date_from,
200         'date_to' : _default_date_to,
201         'state': 'new',
202         'employee_id': _default_employee,
203         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr_timesheet_sheet.sheet', context=c)
204     }
205
206     def _sheet_date(self, cr, uid, ids, forced_user_id=False, context=None):
207         for sheet in self.browse(cr, uid, ids, context=context):
208             new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id
209             if new_user_id:
210                 cr.execute('SELECT id \
211                     FROM hr_timesheet_sheet_sheet \
212                     WHERE (date_from <= %s and %s <= date_to) \
213                         AND user_id=%s \
214                         AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
215                 if cr.fetchall():
216                     return False
217         return True
218
219
220     _constraints = [
221         (_sheet_date, 'You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']),
222     ]
223
224     def action_set_to_draft(self, cr, uid, ids, *args):
225         self.write(cr, uid, ids, {'state': 'draft'})
226         wf_service = netsvc.LocalService('workflow')
227         for id in ids:
228             wf_service.trg_create(uid, self._name, id, cr)
229         return True
230
231     def name_get(self, cr, uid, ids, context=None):
232         if not ids:
233             return []
234         if isinstance(ids, (long, int)):
235             ids = [ids]
236         return [(r['id'], _('Week ')+datetime.strptime(r['date_from'], '%Y-%m-%d').strftime('%U')) \
237                 for r in self.read(cr, uid, ids, ['date_from'],
238                     context=context, load='_classic_write')]
239
240     def unlink(self, cr, uid, ids, context=None):
241         sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
242         for sheet in sheets:
243             if sheet['state'] in ('confirm', 'done'):
244                 raise osv.except_osv(_('Invalid Action!'), _('You cannot delete a timesheet which is already confirmed.'))
245             elif sheet['total_attendance'] <> 0.00:
246                 raise osv.except_osv(_('Invalid Action!'), _('You cannot delete a timesheet which have attendance entries.'))
247         return super(hr_timesheet_sheet, self).unlink(cr, uid, ids, context=context)
248
249     def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
250         department_id =  False
251         user_id = False
252         if employee_id:
253             empl_id = self.pool.get('hr.employee').browse(cr, uid, employee_id, context=context)
254             department_id = empl_id.department_id.id
255             user_id = empl_id.user_id.id
256         return {'value': {'department_id': department_id, 'user_id': user_id,}}
257
258     # ------------------------------------------------
259     # OpenChatter methods and notifications
260     # ------------------------------------------------
261
262     def _needaction_domain_get(self, cr, uid, context=None):
263         emp_obj = self.pool.get('hr.employee')
264         empids = emp_obj.search(cr, uid, [('parent_id.user_id', '=', uid)], context=context)
265         if not empids:
266             return False
267         dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
268         return dom
269
270
271 class account_analytic_line(osv.osv):
272     _inherit = "account.analytic.line"
273
274     def _get_default_date(self, cr, uid, context=None):
275         if context is None:
276             context = {}
277         #get the default date (should be: today)
278         res = super(account_analytic_line, self)._get_default_date(cr, uid, context=context)
279         #if we got the dates from and to from the timesheet and if the default date is in between, we use the default
280         #but if the default isn't included in those dates, we use the date start of the timesheet as default
281         if context.get('timesheet_date_from') and context.get('timesheet_date_to'):
282             if context['timesheet_date_from'] <= res <= context['timesheet_date_to']:
283                 return res
284             return context.get('timesheet_date_from')
285         #if we don't get the dates from the timesheet, we return the default value from super()
286         return res
287
288
289 class hr_timesheet_line(osv.osv):
290     _inherit = "hr.analytic.timesheet"
291
292     def _sheet(self, cursor, user, ids, name, args, context=None):
293         sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
294         res = {}.fromkeys(ids, False)
295         for ts_line in self.browse(cursor, user, ids, context=context):
296             sheet_ids = sheet_obj.search(cursor, user,
297                 [('date_to', '>=', ts_line.date), ('date_from', '<=', ts_line.date),
298                  ('employee_id.user_id', '=', ts_line.user_id.id)],
299                 context=context)
300             if sheet_ids:
301             # [0] because only one sheet possible for an employee between 2 dates
302                 res[ts_line.id] = sheet_obj.name_get(cursor, user, sheet_ids, context=context)[0]
303         return res
304
305     def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
306         ts_line_ids = []
307         for ts in self.browse(cr, uid, ids, context=context):
308             cr.execute("""
309                     SELECT l.id
310                         FROM hr_analytic_timesheet l
311                     INNER JOIN account_analytic_line al
312                         ON (l.line_id = al.id)
313                     WHERE %(date_to)s >= al.date
314                         AND %(date_from)s <= al.date
315                         AND %(user_id)s = al.user_id
316                     GROUP BY l.id""", {'date_from': ts.date_from,
317                                         'date_to': ts.date_to,
318                                         'user_id': ts.employee_id.user_id.id,})
319             ts_line_ids.extend([row[0] for row in cr.fetchall()])
320         return ts_line_ids
321
322     def _get_account_analytic_line(self, cr, uid, ids, context=None):
323         ts_line_ids = self.pool.get('hr.analytic.timesheet').search(cr, uid, [('line_id', 'in', ids)])
324         return ts_line_ids
325
326     _columns = {
327         'sheet_id': fields.function(_sheet, string='Sheet', select="1",
328             type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
329             store={
330                     'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
331                     'account.analytic.line': (_get_account_analytic_line, ['user_id', 'date'], 10),
332                     'hr.analytic.timesheet': (lambda self,cr,uid,ids,context=None: ids, None, 10),
333                   },
334             ),
335     }
336
337     def _check_sheet_state(self, cr, uid, ids, context=None):
338         if context is None:
339             context = {}
340         for timesheet_line in self.browse(cr, uid, ids, context=context):
341             if timesheet_line.sheet_id and timesheet_line.sheet_id.state not in ('draft', 'new'):
342                 return False
343         return True
344
345     _constraints = [
346         (_check_sheet_state, 'You cannot modify an entry in a Confirmed/Done timesheet !', ['state']),
347     ]
348
349     def unlink(self, cr, uid, ids, *args, **kwargs):
350         if isinstance(ids, (int, long)):
351             ids = [ids]
352         self._check(cr, uid, ids)
353         return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
354
355     def _check(self, cr, uid, ids):
356         for att in self.browse(cr, uid, ids):
357             if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
358                 raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet.'))
359         return True
360
361     def multi_on_change_account_id(self, cr, uid, ids, account_ids, context=None):
362         return dict([(el, self.on_change_account_id(cr, uid, ids, el, context.get('user_id', uid))) for el in account_ids])
363
364
365 hr_timesheet_line()
366
367 class hr_attendance(osv.osv):
368     _inherit = "hr.attendance"
369
370     def _get_default_date(self, cr, uid, context=None):
371         if context is None:
372             context = {}
373         if 'name' in context:
374             return context['name'] + time.strftime(' %H:%M:%S')
375         return time.strftime('%Y-%m-%d %H:%M:%S')
376
377     def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
378         attendance_ids = []
379         for ts in self.browse(cr, uid, ids, context=context):
380             cr.execute("""
381                         SELECT a.id
382                           FROM hr_attendance a
383                          INNER JOIN hr_employee e
384                                INNER JOIN resource_resource r
385                                        ON (e.resource_id = r.id)
386                             ON (a.employee_id = e.id)
387                         WHERE %(date_to)s >= date_trunc('day', a.name)
388                               AND %(date_from)s <= a.name
389                               AND %(user_id)s = r.user_id
390                          GROUP BY a.id""", {'date_from': ts.date_from,
391                                             'date_to': ts.date_to,
392                                             'user_id': ts.employee_id.user_id.id,})
393             attendance_ids.extend([row[0] for row in cr.fetchall()])
394         return attendance_ids
395
396     def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None):
397         """ Simulate timesheet in employee timezone
398
399         Return the attendance datetime as date in string format in employee
400         tz converted from utc timezone as we consider date of employee
401         timesheet is in employee timezone
402         """
403         employee_obj = self.pool['hr.employee']
404
405         tz = False
406         if employee_id:
407             employee = employee_obj.browse(cr, uid, employee_id, context=context)
408             tz = employee.user_id.partner_id.tz
409
410         att_tz = timezone(tz or 'utc')
411
412         attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT)
413         att_tz_dt = pytz.utc.localize(attendance_dt)
414         att_tz_dt = att_tz_dt.astimezone(att_tz)
415         # We take only the date omiting the hours as we compare with timesheet
416         # date_from which is a date format thus using hours would lead to
417         # be out of scope of timesheet
418         att_tz_date_str = datetime.strftime(att_tz_dt, DEFAULT_SERVER_DATE_FORMAT)
419         return att_tz_date_str
420
421     def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None):
422
423         sheet_obj = self.pool['hr_timesheet_sheet.sheet']
424         if not date:
425             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
426
427         att_tz_date_str = self._get_attendance_employee_tz(
428                 cr, uid, employee_id,
429                 date=date, context=context)
430         sheet_ids = sheet_obj.search(cr, uid,
431             [('date_from', '<=', att_tz_date_str),
432              ('date_to', '>=', att_tz_date_str),
433              ('employee_id', '=', employee_id)],
434             context=context)
435         return sheet_ids and sheet_ids[0] or False
436
437     def _sheet(self, cursor, user, ids, name, args, context=None):
438         res = {}.fromkeys(ids, False)
439         for attendance in self.browse(cursor, user, ids, context=context):
440             res[attendance.id] = self._get_current_sheet(
441                     cursor, user, attendance.employee_id.id, attendance.name,
442                     context=context)
443         return res
444
445     _columns = {
446         'sheet_id': fields.function(_sheet, string='Sheet',
447             type='many2one', relation='hr_timesheet_sheet.sheet',
448             store={
449                       'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
450                       'hr.attendance': (lambda self,cr,uid,ids,context=None: ids, ['employee_id', 'name', 'day'], 10),
451                   },
452             )
453     }
454     _defaults = {
455         'name': _get_default_date,
456     }
457
458     def create(self, cr, uid, vals, context=None):
459         if context is None:
460             context = {}
461
462         sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context)
463         if sheet_id:
464             att_tz_date_str = self._get_attendance_employee_tz(
465                     cr, uid, vals.get('employee_id'),
466                    date=vals.get('name'), context=context)
467             ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, sheet_id, context=context)
468             if ts.state not in ('draft', 'new'):
469                 raise osv.except_osv(_('Error!'), _('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.'))
470             elif ts.date_from > att_tz_date_str or ts.date_to < att_tz_date_str:
471                 raise osv.except_osv(_('User Error!'), _('You can not enter an attendance date outside the current timesheet dates.'))
472         return super(hr_attendance,self).create(cr, uid, vals, context=context)
473
474     def unlink(self, cr, uid, ids, *args, **kwargs):
475         if isinstance(ids, (int, long)):
476             ids = [ids]
477         self._check(cr, uid, ids)
478         return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
479
480     def write(self, cr, uid, ids, vals, context=None):
481         if context is None:
482             context = {}
483         if isinstance(ids, (int, long)):
484             ids = [ids]
485         self._check(cr, uid, ids)
486         res = super(hr_attendance,self).write(cr, uid, ids, vals, context=context)
487         if 'sheet_id' in context:
488             for attendance in self.browse(cr, uid, ids, context=context):
489                 if context['sheet_id'] != attendance.sheet_id.id:
490                     raise osv.except_osv(_('User Error!'), _('You cannot enter an attendance ' \
491                             'date outside the current timesheet dates.'))
492         return res
493
494     def _check(self, cr, uid, ids):
495         for att in self.browse(cr, uid, ids):
496             if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
497                 raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet'))
498         return True
499
500 hr_attendance()
501
502 class hr_timesheet_sheet_sheet_day(osv.osv):
503     _name = "hr_timesheet_sheet.sheet.day"
504     _description = "Timesheets by Period"
505     _auto = False
506     _order='name'
507     _columns = {
508         'name': fields.date('Date', readonly=True),
509         'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, select="1"),
510         'total_timesheet': fields.float('Total Timesheet', readonly=True),
511         'total_attendance': fields.float('Attendance', readonly=True),
512         'total_difference': fields.float('Difference', readonly=True),
513     }
514
515     def init(self, cr):
516         cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
517             SELECT
518                 id,
519                 name,
520                 sheet_id,
521                 total_timesheet,
522                 total_attendance,
523                 cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
524             FROM
525                 ((
526                     SELECT
527                         MAX(id) as id,
528                         name,
529                         sheet_id,
530                         SUM(total_timesheet) as total_timesheet,
531                         CASE WHEN SUM(total_attendance) < 0
532                             THEN (SUM(total_attendance) +
533                                 CASE WHEN current_date <> name
534                                     THEN 1440
535                                     ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC') * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC')
536                                 END
537                                 )
538                             ELSE SUM(total_attendance)
539                         END /60  as total_attendance
540                     FROM
541                         ((
542                             select
543                                 min(hrt.id) as id,
544                                 l.date::date as name,
545                                 s.id as sheet_id,
546                                 sum(l.unit_amount) as total_timesheet,
547                                 0.0 as total_attendance
548                             from
549                                 hr_analytic_timesheet hrt
550                                 JOIN account_analytic_line l ON l.id = hrt.line_id
551                                 LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = hrt.sheet_id
552                             group by l.date::date, s.id
553                         ) union (
554                             select
555                                 -min(a.id) as id,
556                                 a.name::date as name,
557                                 s.id as sheet_id,
558                                 0.0 as total_timesheet,
559                                 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
560                             from
561                                 hr_attendance a
562                                 LEFT JOIN hr_timesheet_sheet_sheet s
563                                 ON s.id = a.sheet_id
564                             WHERE action in ('sign_in', 'sign_out')
565                             group by a.name::date, s.id
566                         )) AS foo
567                         GROUP BY name, sheet_id
568                 )) AS bar""")
569
570 hr_timesheet_sheet_sheet_day()
571
572
573 class hr_timesheet_sheet_sheet_account(osv.osv):
574     _name = "hr_timesheet_sheet.sheet.account"
575     _description = "Timesheets by Period"
576     _auto = False
577     _order='name'
578     _columns = {
579         'name': fields.many2one('account.analytic.account', 'Project / Analytic Account', readonly=True),
580         'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True),
581         'total': fields.float('Total Time', digits=(16,2), readonly=True),
582         'invoice_rate': fields.many2one('hr_timesheet_invoice.factor', 'Invoice rate', readonly=True),
583         }
584
585     def init(self, cr):
586         cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
587             select
588                 min(hrt.id) as id,
589                 l.account_id as name,
590                 s.id as sheet_id,
591                 sum(l.unit_amount) as total,
592                 l.to_invoice as invoice_rate
593             from
594                 hr_analytic_timesheet hrt
595                 left join (account_analytic_line l
596                     LEFT JOIN hr_timesheet_sheet_sheet s
597                         ON (s.date_to >= l.date
598                             AND s.date_from <= l.date
599                             AND s.user_id = l.user_id))
600                     on (l.id = hrt.line_id)
601             group by l.account_id, s.id, l.to_invoice
602         )""")
603
604 hr_timesheet_sheet_sheet_account()
605
606
607
608 class res_company(osv.osv):
609     _inherit = 'res.company'
610     _columns = {
611         'timesheet_range': fields.selection(
612             [('day','Day'),('week','Week'),('month','Month')], 'Timesheet range',
613             help="Periodicity on which you validate your timesheets."),
614         'timesheet_max_difference': fields.float('Timesheet allowed difference(Hours)',
615             help="Allowed difference in hours between the sign in/out and the timesheet " \
616                  "computation for one sheet. Set this to 0 if you do not want any control."),
617     }
618     _defaults = {
619         'timesheet_range': lambda *args: 'week',
620         'timesheet_max_difference': lambda *args: 0.0
621     }
622
623 res_company()
624
625 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: