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