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 _
32 class hr_timesheet_sheet(osv.osv):
33 _name = "hr_timesheet_sheet.sheet"
34 _inherit = "mail.thread"
35 _table = 'hr_timesheet_sheet_sheet'
37 _description="Timesheet"
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
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,
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
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')])
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.'))
65 def copy(self, cr, uid, ids, *args, **argv):
66 raise osv.except_osv(_('Error!'), _('You cannot duplicate a timesheet.'))
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)
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
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)'))
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']
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))
115 date_attendances.append((0, False, att_tuple))
116 date_attendances.sort()
117 return [att[2] for att in date_attendances]
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])
128 raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
131 def attendance_action_change(self, cr, uid, ids, context=None):
132 hr_employee = self.pool.get('hr.employee')
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)
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',
147 readonly=True, states={
148 'draft': [('readonly', False)],
149 'new': [('readonly', False)]}
151 'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
152 'state' : fields.selection([
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'),
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'
174 return time.strftime('%Y-%m-01')
176 return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
178 return time.strftime('%Y-01-01')
179 return time.strftime('%Y-%m-%d')
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'
185 return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
187 return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
189 return time.strftime('%Y-12-31')
190 return time.strftime('%Y-%m-%d')
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
197 'date_from' : _default_date_from,
198 'date_to' : _default_date_to,
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)
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
208 cr.execute('SELECT id \
209 FROM hr_timesheet_sheet_sheet \
210 WHERE (date_from <= %s and %s <= date_to) \
212 AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
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']),
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)
227 def name_get(self, cr, uid, ids, context=None):
230 if isinstance(ids, (long, int)):
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')]
236 def unlink(self, cr, uid, ids, context=None):
237 sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
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)
245 def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
246 department_id = False
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,}}
254 # ------------------------------------------------
255 # OpenChatter methods and notifications
256 # ------------------------------------------------
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)
263 dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
267 class account_analytic_line(osv.osv):
268 _inherit = "account.analytic.line"
270 def _get_default_date(self, cr, uid, context=None):
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']:
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()
285 class hr_timesheet_line(osv.osv):
286 _inherit = "hr.analytic.timesheet"
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)],
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]
301 def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
303 for ts in self.browse(cr, uid, ids, context=context):
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()])
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)])
323 'sheet_id': fields.function(_sheet, string='Sheet', select="1",
324 type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
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),
333 def _check_sheet_state(self, cr, uid, ids, context=None):
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'):
342 (_check_sheet_state, 'You cannot modify an entry in a Confirmed/Done timesheet !', ['state']),
345 def unlink(self, cr, uid, ids, *args, **kwargs):
346 if isinstance(ids, (int, long)):
348 self._check(cr, uid, ids)
349 return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
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.'))
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])
362 class hr_attendance(osv.osv):
363 _inherit = "hr.attendance"
365 def _get_default_date(self, cr, uid, context=None):
368 if 'name' in context:
369 return context['name'] + time.strftime(' %H:%M:%S')
370 return time.strftime('%Y-%m-%d %H:%M:%S')
372 def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
374 for ts in self.browse(cr, uid, ids, context=context):
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
391 def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None):
392 """ Simulate timesheet in employee timezone
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
398 employee_obj = self.pool['hr.employee']
402 employee = employee_obj.browse(cr, uid, employee_id, context=context)
403 tz = employee.user_id.partner_id.tz
405 att_tz = timezone(tz or 'utc')
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
416 def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None):
418 sheet_obj = self.pool['hr_timesheet_sheet.sheet']
420 date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
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
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,
441 'sheet_id': fields.function(_sheet, string='Sheet',
442 type='many2one', relation='hr_timesheet_sheet.sheet',
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),
450 'name': _get_default_date,
453 def create(self, cr, uid, vals, context=None):
457 sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context)
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)
469 def unlink(self, cr, uid, ids, *args, **kwargs):
470 if isinstance(ids, (int, long)):
472 self._check(cr, uid, ids)
473 return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
475 def write(self, cr, uid, ids, vals, context=None):
478 if isinstance(ids, (int, long)):
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.'))
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'))
496 class hr_timesheet_sheet_sheet_day(osv.osv):
497 _name = "hr_timesheet_sheet.sheet.day"
498 _description = "Timesheets by Period"
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),
510 cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
517 cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
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
529 ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC') * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC')
532 ELSE SUM(total_attendance)
533 END /60 as total_attendance
538 l.date::date as name,
540 sum(l.unit_amount) as total_timesheet,
541 0.0 as total_attendance
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
550 a.name::date as name,
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
556 LEFT JOIN hr_timesheet_sheet_sheet s
558 WHERE action in ('sign_in', 'sign_out')
559 group by a.name::date, s.id
561 GROUP BY name, sheet_id
566 class hr_timesheet_sheet_sheet_account(osv.osv):
567 _name = "hr_timesheet_sheet.sheet.account"
568 _description = "Timesheets by Period"
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),
579 cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
582 l.account_id as name,
584 sum(l.unit_amount) as total,
585 l.to_invoice as invoice_rate
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
600 class res_company(osv.osv):
601 _inherit = 'res.company'
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."),
611 'timesheet_range': lambda *args: 'week',
612 'timesheet_max_difference': lambda *args: 0.0
616 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: