1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
23 from datetime import datetime
24 from dateutil.relativedelta import relativedelta
25 from pytz import timezone
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
33 class hr_timesheet_sheet(osv.osv):
34 _name = "hr_timesheet_sheet.sheet"
35 _inherit = "mail.thread"
36 _table = 'hr_timesheet_sheet_sheet'
38 _description="Timesheet"
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
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,
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
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')])
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.'))
66 def copy(self, cr, uid, ids, *args, **argv):
67 raise osv.except_osv(_('Error!'), _('You cannot duplicate a timesheet.'))
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)
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
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)'))
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']
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))
116 date_attendances.append((0, False, att_tuple))
117 date_attendances.sort()
118 return [att[2] for att in date_attendances]
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)
130 raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
133 def attendance_action_change(self, cr, uid, ids, context=None):
134 hr_employee = self.pool.get('hr.employee')
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)
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',
149 readonly=True, states={
150 'draft': [('readonly', False)],
151 'new': [('readonly', False)]}
153 'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
154 'state' : fields.selection([
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'),
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'
176 return time.strftime('%Y-%m-01')
178 return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
180 return time.strftime('%Y-01-01')
181 return time.strftime('%Y-%m-%d')
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'
187 return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
189 return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
191 return time.strftime('%Y-12-31')
192 return time.strftime('%Y-%m-%d')
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
199 'date_from' : _default_date_from,
200 'date_to' : _default_date_to,
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)
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
210 cr.execute('SELECT id \
211 FROM hr_timesheet_sheet_sheet \
212 WHERE (date_from <= %s and %s <= date_to) \
214 AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
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']),
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')
228 wf_service.trg_create(uid, self._name, id, cr)
231 def name_get(self, cr, uid, ids, context=None):
234 if isinstance(ids, (long, int)):
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')]
240 def unlink(self, cr, uid, ids, context=None):
241 sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
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)
249 def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
250 department_id = False
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,}}
258 # ------------------------------------------------
259 # OpenChatter methods and notifications
260 # ------------------------------------------------
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)
267 dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
271 class account_analytic_line(osv.osv):
272 _inherit = "account.analytic.line"
274 def _get_default_date(self, cr, uid, context=None):
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']:
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()
289 class hr_timesheet_line(osv.osv):
290 _inherit = "hr.analytic.timesheet"
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)],
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]
305 def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
307 for ts in self.browse(cr, uid, ids, context=context):
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()])
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)])
327 'sheet_id': fields.function(_sheet, string='Sheet', select="1",
328 type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
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),
337 def _check_sheet_state(self, cr, uid, ids, context=None):
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'):
346 (_check_sheet_state, 'You cannot modify an entry in a Confirmed/Done timesheet !', ['state']),
349 def unlink(self, cr, uid, ids, *args, **kwargs):
350 if isinstance(ids, (int, long)):
352 self._check(cr, uid, ids)
353 return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
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.'))
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])
367 class hr_attendance(osv.osv):
368 _inherit = "hr.attendance"
370 def _get_default_date(self, cr, uid, context=None):
373 if 'name' in context:
374 return context['name'] + time.strftime(' %H:%M:%S')
375 return time.strftime('%Y-%m-%d %H:%M:%S')
377 def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
379 for ts in self.browse(cr, uid, ids, context=context):
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
396 def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None):
397 """ Simulate timesheet in employee timezone
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
403 employee_obj = self.pool['hr.employee']
407 employee = employee_obj.browse(cr, uid, employee_id, context=context)
408 tz = employee.user_id.partner_id.tz
410 att_tz = timezone(tz or 'utc')
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
421 def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None):
423 sheet_obj = self.pool['hr_timesheet_sheet.sheet']
425 date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
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)],
435 return sheet_ids and sheet_ids[0] or False
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,
446 'sheet_id': fields.function(_sheet, string='Sheet',
447 type='many2one', relation='hr_timesheet_sheet.sheet',
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),
455 'name': _get_default_date,
458 def create(self, cr, uid, vals, context=None):
462 sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context)
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)
474 def unlink(self, cr, uid, ids, *args, **kwargs):
475 if isinstance(ids, (int, long)):
477 self._check(cr, uid, ids)
478 return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
480 def write(self, cr, uid, ids, vals, context=None):
483 if isinstance(ids, (int, long)):
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.'))
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'))
502 class hr_timesheet_sheet_sheet_day(osv.osv):
503 _name = "hr_timesheet_sheet.sheet.day"
504 _description = "Timesheets by Period"
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),
516 cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
523 cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
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
535 ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC') * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC')
538 ELSE SUM(total_attendance)
539 END /60 as total_attendance
544 l.date::date as name,
546 sum(l.unit_amount) as total_timesheet,
547 0.0 as total_attendance
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
556 a.name::date as name,
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
562 LEFT JOIN hr_timesheet_sheet_sheet s
564 WHERE action in ('sign_in', 'sign_out')
565 group by a.name::date, s.id
567 GROUP BY name, sheet_id
570 hr_timesheet_sheet_sheet_day()
573 class hr_timesheet_sheet_sheet_account(osv.osv):
574 _name = "hr_timesheet_sheet.sheet.account"
575 _description = "Timesheets by Period"
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),
586 cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
589 l.account_id as name,
591 sum(l.unit_amount) as total,
592 l.to_invoice as invoice_rate
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
604 hr_timesheet_sheet_sheet_account()
608 class res_company(osv.osv):
609 _inherit = 'res.company'
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."),
619 'timesheet_range': lambda *args: 'week',
620 'timesheet_max_difference': lambda *args: 0.0
625 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: