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', 'ir.needaction_mixin']
35 _table = 'hr_timesheet_sheet_sheet'
37 _description = "Timesheet"
41 'hr_timesheet_sheet.mt_timesheet_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirm',
42 'hr_timesheet_sheet.mt_timesheet_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
46 def _total(self, cr, uid, ids, name, args, context=None):
47 """ Compute the attendances, analytic lines timesheets and differences between them
48 for all the days of a timesheet and the current day
52 for sheet in self.browse(cr, uid, ids, context=context or {}):
53 res.setdefault(sheet.id, {
54 'total_attendance': 0.0,
55 'total_timesheet': 0.0,
56 'total_difference': 0.0,
58 for period in sheet.period_ids:
59 res[sheet.id]['total_attendance'] += period.total_attendance
60 res[sheet.id]['total_timesheet'] += period.total_timesheet
61 res[sheet.id]['total_difference'] += period.total_attendance - period.total_timesheet
64 def check_employee_attendance_state(self, cr, uid, sheet_id, context=None):
65 ids_signin = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_in')])
66 ids_signout = self.pool.get('hr.attendance').search(cr,uid,[('sheet_id', '=', sheet_id),('action','=','sign_out')])
68 if len(ids_signin) != len(ids_signout):
69 raise osv.except_osv(('Warning!'),_('The timesheet cannot be validated as it does not contain an equal number of sign ins and sign outs.'))
72 def copy(self, cr, uid, ids, *args, **argv):
73 raise osv.except_osv(_('Error!'), _('You cannot duplicate a timesheet.'))
75 def create(self, cr, uid, vals, context=None):
76 if 'employee_id' in vals:
77 if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).user_id:
78 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link him/her to a user.'))
79 if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).product_id:
80 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product, like \'Consultant\'.'))
81 if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).journal_id:
82 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\'.'))
83 if vals.get('attendances_ids'):
84 # If attendances, we sort them by date asc before writing them, to satisfy the alternance constraint
85 vals['attendances_ids'] = self.sort_attendances(cr, uid, vals['attendances_ids'], context=context)
86 return super(hr_timesheet_sheet, self).create(cr, uid, vals, context=context)
88 def write(self, cr, uid, ids, vals, context=None):
89 if 'employee_id' in vals:
90 new_user_id = self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).user_id.id or False
92 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link him/her to a user.'))
93 if not self._sheet_date(cr, uid, ids, forced_user_id=new_user_id, context=context):
94 raise osv.except_osv(_('Error!'), _('You cannot have 2 timesheets that overlap!\nYou should use the menu \'My Timesheet\' to avoid this problem.'))
95 if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).product_id:
96 raise osv.except_osv(_('Error!'), _('In order to create a timesheet for this employee, you must link the employee to a product.'))
97 if not self.pool.get('hr.employee').browse(cr, uid, vals['employee_id'], context=context).journal_id:
98 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\'.'))
99 if vals.get('attendances_ids'):
100 # If attendances, we sort them by date asc before writing them, to satisfy the alternance constraint
101 # In addition to the date order, deleting attendances are done before inserting attendances
102 vals['attendances_ids'] = self.sort_attendances(cr, uid, vals['attendances_ids'], context=context)
103 res = super(hr_timesheet_sheet, self).write(cr, uid, ids, vals, context=context)
104 if vals.get('attendances_ids'):
105 for timesheet in self.browse(cr, uid, ids):
106 if not self.pool['hr.attendance']._altern_si_so(cr, uid, [att.id for att in timesheet.attendances_ids]):
107 raise osv.except_osv(_('Warning !'), _('Error ! Sign in (resp. Sign out) must follow Sign out (resp. Sign in)'))
110 def sort_attendances(self, cr, uid, attendance_tuples, context=None):
111 date_attendances = []
112 for att_tuple in attendance_tuples:
113 if att_tuple[0] in [0,1,4]:
114 if att_tuple[0] in [0,1]:
115 if att_tuple[2] and att_tuple[2].has_key('name'):
116 name = att_tuple[2]['name']
118 name = self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name
120 name = self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name
121 date_attendances.append((1, name, att_tuple))
122 elif att_tuple[0] in [2,3]:
123 date_attendances.append((0, self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name, att_tuple))
125 date_attendances.append((0, False, att_tuple))
126 date_attendances.sort()
127 return [att[2] for att in date_attendances]
129 def button_confirm(self, cr, uid, ids, context=None):
130 for sheet in self.browse(cr, uid, ids, context=context):
131 if sheet.employee_id and sheet.employee_id.parent_id and sheet.employee_id.parent_id.user_id:
132 self.message_subscribe_users(cr, uid, [sheet.id], user_ids=[sheet.employee_id.parent_id.user_id.id], context=context)
133 self.check_employee_attendance_state(cr, uid, sheet.id, context=context)
134 di = sheet.user_id.company_id.timesheet_max_difference
135 if (abs(sheet.total_difference) < di) or not di:
136 sheet.signal_workflow('confirm')
138 raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
141 def attendance_action_change(self, cr, uid, ids, context=None):
142 hr_employee = self.pool.get('hr.employee')
144 for sheet in self.browse(cr, uid, ids, context=context):
145 if sheet.employee_id.id not in employee_ids: employee_ids.append(sheet.employee_id.id)
146 return hr_employee.attendance_action_change(cr, uid, employee_ids, context=context)
148 def _count_all(self, cr, uid, ids, field_name, arg, context=None):
149 Timesheet = self.pool['hr.analytic.timesheet']
150 Attendance = self.pool['hr.attendance']
153 'timesheet_activity_count': Timesheet.search_count(cr,uid, [('sheet_id','=', sheet_id)], context=context),
154 'attendance_count': Attendance.search_count(cr,uid, [('sheet_id', '=', sheet_id)], context=context)
160 'name': fields.char('Note', select=1,
161 states={'confirm':[('readonly', True)], 'done':[('readonly', True)]}),
162 'employee_id': fields.many2one('hr.employee', 'Employee', required=True),
163 '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)]}),
164 'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
165 'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
166 'timesheet_ids' : fields.one2many('hr.analytic.timesheet', 'sheet_id',
168 readonly=True, states={
169 'draft': [('readonly', False)],
170 'new': [('readonly', False)]}
172 'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
173 'state' : fields.selection([
176 ('confirm','Waiting Approval'),
177 ('done','Approved')], 'Status', select=True, required=True, readonly=True,
178 track_visibility='onchange',
179 help=' * The \'Draft\' status is used when a user is encoding a new and unconfirmed timesheet. \
180 \n* The \'Confirmed\' status is used for to confirm the timesheet by user. \
181 \n* The \'Done\' status is used when users timesheet is accepted by his/her senior.'),
182 'state_attendance' : fields.related('employee_id', 'state', type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Current Status', readonly=True),
183 'total_attendance': fields.function(_total, method=True, string='Total Attendance', multi="_total"),
184 'total_timesheet': fields.function(_total, method=True, string='Total Timesheet', multi="_total"),
185 'total_difference': fields.function(_total, method=True, string='Difference', multi="_total"),
186 'period_ids': fields.one2many('hr_timesheet_sheet.sheet.day', 'sheet_id', 'Period', readonly=True),
187 'account_ids': fields.one2many('hr_timesheet_sheet.sheet.account', 'sheet_id', 'Analytic accounts', readonly=True),
188 'company_id': fields.many2one('res.company', 'Company'),
189 'department_id':fields.many2one('hr.department','Department'),
190 'timesheet_activity_count': fields.function(_count_all, type='integer', string='Timesheet Activities', multi=True),
191 'attendance_count': fields.function(_count_all, type='integer', string="Attendances", multi=True),
194 def _default_date_from(self, cr, uid, context=None):
195 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
196 r = user.company_id and user.company_id.timesheet_range or 'month'
198 return time.strftime('%Y-%m-01')
200 return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
202 return time.strftime('%Y-01-01')
203 return time.strftime('%Y-%m-%d')
205 def _default_date_to(self, cr, uid, context=None):
206 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
207 r = user.company_id and user.company_id.timesheet_range or 'month'
209 return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
211 return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
213 return time.strftime('%Y-12-31')
214 return time.strftime('%Y-%m-%d')
216 def _default_employee(self, cr, uid, context=None):
217 emp_ids = self.pool.get('hr.employee').search(cr, uid, [('user_id','=',uid)], context=context)
218 return emp_ids and emp_ids[0] or False
221 'date_from' : _default_date_from,
222 'date_to' : _default_date_to,
224 'employee_id': _default_employee,
225 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr_timesheet_sheet.sheet', context=c)
228 def _sheet_date(self, cr, uid, ids, forced_user_id=False, context=None):
229 for sheet in self.browse(cr, uid, ids, context=context):
230 new_user_id = forced_user_id or sheet.user_id and sheet.user_id.id
232 cr.execute('SELECT id \
233 FROM hr_timesheet_sheet_sheet \
234 WHERE (date_from <= %s and %s <= date_to) \
236 AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
243 (_sheet_date, 'You cannot have 2 timesheets that overlap!\nPlease use the menu \'My Current Timesheet\' to avoid this problem.', ['date_from','date_to']),
246 def action_set_to_draft(self, cr, uid, ids, *args):
247 self.write(cr, uid, ids, {'state': 'draft'})
248 self.create_workflow(cr, uid, ids)
251 def name_get(self, cr, uid, ids, context=None):
254 if isinstance(ids, (long, int)):
256 return [(r['id'], _('Week ')+datetime.strptime(r['date_from'], '%Y-%m-%d').strftime('%U')) \
257 for r in self.read(cr, uid, ids, ['date_from'],
258 context=context, load='_classic_write')]
260 def unlink(self, cr, uid, ids, context=None):
261 sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
263 if sheet['state'] in ('confirm', 'done'):
264 raise osv.except_osv(_('Invalid Action!'), _('You cannot delete a timesheet which is already confirmed.'))
265 elif sheet['total_attendance'] <> 0.00:
266 raise osv.except_osv(_('Invalid Action!'), _('You cannot delete a timesheet which have attendance entries.'))
267 return super(hr_timesheet_sheet, self).unlink(cr, uid, ids, context=context)
269 def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
270 department_id = False
273 empl_id = self.pool.get('hr.employee').browse(cr, uid, employee_id, context=context)
274 department_id = empl_id.department_id.id
275 user_id = empl_id.user_id.id
276 return {'value': {'department_id': department_id, 'user_id': user_id,}}
278 # ------------------------------------------------
279 # OpenChatter methods and notifications
280 # ------------------------------------------------
282 def _needaction_domain_get(self, cr, uid, context=None):
283 emp_obj = self.pool.get('hr.employee')
284 empids = emp_obj.search(cr, uid, [('parent_id.user_id', '=', uid)], context=context)
287 dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
291 class account_analytic_line(osv.osv):
292 _inherit = "account.analytic.line"
294 def _get_default_date(self, cr, uid, context=None):
297 #get the default date (should be: today)
298 res = super(account_analytic_line, self)._get_default_date(cr, uid, context=context)
299 #if we got the dates from and to from the timesheet and if the default date is in between, we use the default
300 #but if the default isn't included in those dates, we use the date start of the timesheet as default
301 if context.get('timesheet_date_from') and context.get('timesheet_date_to'):
302 if context['timesheet_date_from'] <= res <= context['timesheet_date_to']:
304 return context.get('timesheet_date_from')
305 #if we don't get the dates from the timesheet, we return the default value from super()
308 class account_analytic_account(osv.osv):
309 _inherit = "account.analytic.account"
311 def name_create(self, cr, uid, name, context=None):
314 group_template_required = self.pool['res.users'].has_group(cr, uid, 'account_analytic_analysis.group_template_required')
315 if not context.get('default_invoice_on_timesheets') or group_template_required:
316 return super(account_analytic_account, self).name_create(cr, uid, name, context=context)
317 rec_id = self.create(cr, uid, {self._rec_name: name}, context)
318 return self.name_get(cr, uid, [rec_id], context)[0]
320 class hr_timesheet_line(osv.osv):
321 _inherit = "hr.analytic.timesheet"
323 def _sheet(self, cursor, user, ids, name, args, context=None):
324 sheet_obj = self.pool.get('hr_timesheet_sheet.sheet')
325 res = {}.fromkeys(ids, False)
326 for ts_line in self.browse(cursor, user, ids, context=context):
327 sheet_ids = sheet_obj.search(cursor, user,
328 [('date_to', '>=', ts_line.date), ('date_from', '<=', ts_line.date),
329 ('employee_id.user_id', '=', ts_line.user_id.id)],
332 # [0] because only one sheet possible for an employee between 2 dates
333 res[ts_line.id] = sheet_obj.name_get(cursor, user, sheet_ids, context=context)[0]
336 def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
338 for ts in self.browse(cr, uid, ids, context=context):
341 FROM hr_analytic_timesheet l
342 INNER JOIN account_analytic_line al
343 ON (l.line_id = al.id)
344 WHERE %(date_to)s >= al.date
345 AND %(date_from)s <= al.date
346 AND %(user_id)s = al.user_id
347 GROUP BY l.id""", {'date_from': ts.date_from,
348 'date_to': ts.date_to,
349 'user_id': ts.employee_id.user_id.id,})
350 ts_line_ids.extend([row[0] for row in cr.fetchall()])
353 def _get_account_analytic_line(self, cr, uid, ids, context=None):
354 ts_line_ids = self.pool.get('hr.analytic.timesheet').search(cr, uid, [('line_id', 'in', ids)])
358 'sheet_id': fields.function(_sheet, string='Sheet', select="1",
359 type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
361 'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
362 'account.analytic.line': (_get_account_analytic_line, ['user_id', 'date'], 10),
363 'hr.analytic.timesheet': (lambda self,cr,uid,ids,context=None: ids, None, 10),
368 def _check_sheet_state(self, cr, uid, ids, context=None):
371 for timesheet_line in self.browse(cr, uid, ids, context=context):
372 if timesheet_line.sheet_id and timesheet_line.sheet_id.state not in ('draft', 'new'):
377 (_check_sheet_state, 'You cannot modify an entry in a Confirmed/Done timesheet !', ['state']),
380 def unlink(self, cr, uid, ids, *args, **kwargs):
381 if isinstance(ids, (int, long)):
383 self._check(cr, uid, ids)
384 return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
386 def _check(self, cr, uid, ids):
387 for att in self.browse(cr, uid, ids):
388 if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
389 raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet.'))
392 def multi_on_change_account_id(self, cr, uid, ids, account_ids, context=None):
393 return dict([(el, self.on_change_account_id(cr, uid, ids, el, context.get('user_id', uid))) for el in account_ids])
397 class hr_attendance(osv.osv):
398 _inherit = "hr.attendance"
400 def _get_default_date(self, cr, uid, context=None):
403 if 'name' in context:
404 return context['name'] + time.strftime(' %H:%M:%S')
405 return time.strftime('%Y-%m-%d %H:%M:%S')
407 def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
409 for ts in self.browse(cr, uid, ids, context=context):
413 INNER JOIN hr_employee e
414 INNER JOIN resource_resource r
415 ON (e.resource_id = r.id)
416 ON (a.employee_id = e.id)
417 WHERE %(date_to)s >= date_trunc('day', a.name)
418 AND %(date_from)s <= a.name
419 AND %(user_id)s = r.user_id
420 GROUP BY a.id""", {'date_from': ts.date_from,
421 'date_to': ts.date_to,
422 'user_id': ts.employee_id.user_id.id,})
423 attendance_ids.extend([row[0] for row in cr.fetchall()])
424 return attendance_ids
426 def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None):
427 """ Simulate timesheet in employee timezone
429 Return the attendance date in string format in the employee
430 tz converted from utc timezone as we consider date of employee
431 timesheet is in employee timezone
433 employee_obj = self.pool['hr.employee']
437 employee = employee_obj.browse(cr, uid, employee_id, context=context)
438 tz = employee.user_id.partner_id.tz
441 date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
443 att_tz = timezone(tz or 'utc')
445 attendance_dt = datetime.strptime(date, DEFAULT_SERVER_DATETIME_FORMAT)
446 att_tz_dt = pytz.utc.localize(attendance_dt)
447 att_tz_dt = att_tz_dt.astimezone(att_tz)
448 # We take only the date omiting the hours as we compare with timesheet
449 # date_from which is a date format thus using hours would lead to
450 # be out of scope of timesheet
451 att_tz_date_str = datetime.strftime(att_tz_dt, DEFAULT_SERVER_DATE_FORMAT)
452 return att_tz_date_str
454 def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None):
456 sheet_obj = self.pool['hr_timesheet_sheet.sheet']
458 date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
460 att_tz_date_str = self._get_attendance_employee_tz(
461 cr, uid, employee_id,
462 date=date, context=context)
463 sheet_ids = sheet_obj.search(cr, uid,
464 [('date_from', '<=', att_tz_date_str),
465 ('date_to', '>=', att_tz_date_str),
466 ('employee_id', '=', employee_id)],
467 limit=1, context=context)
468 return sheet_ids and sheet_ids[0] or False
470 def _sheet(self, cursor, user, ids, name, args, context=None):
471 res = {}.fromkeys(ids, False)
472 for attendance in self.browse(cursor, user, ids, context=context):
473 res[attendance.id] = self._get_current_sheet(
474 cursor, user, attendance.employee_id.id, attendance.name,
479 'sheet_id': fields.function(_sheet, string='Sheet',
480 type='many2one', relation='hr_timesheet_sheet.sheet',
482 'hr_timesheet_sheet.sheet': (_get_hr_timesheet_sheet, ['employee_id', 'date_from', 'date_to'], 10),
483 'hr.attendance': (lambda self,cr,uid,ids,context=None: ids, ['employee_id', 'name', 'day'], 10),
488 'name': _get_default_date,
491 def create(self, cr, uid, vals, context=None):
495 sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context)
497 att_tz_date_str = self._get_attendance_employee_tz(
498 cr, uid, vals.get('employee_id'),
499 date=vals.get('name'), context=context)
500 ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, sheet_id, context=context)
501 if ts.state not in ('draft', 'new'):
502 raise osv.except_osv(_('Error!'), _('You can not enter an attendance in a submitted timesheet. Ask your manager to reset it before adding attendance.'))
503 elif ts.date_from > att_tz_date_str or ts.date_to < att_tz_date_str:
504 raise osv.except_osv(_('User Error!'), _('You can not enter an attendance date outside the current timesheet dates.'))
505 return super(hr_attendance,self).create(cr, uid, vals, context=context)
507 def unlink(self, cr, uid, ids, *args, **kwargs):
508 if isinstance(ids, (int, long)):
510 self._check(cr, uid, ids)
511 return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
513 def write(self, cr, uid, ids, vals, context=None):
516 if isinstance(ids, (int, long)):
518 self._check(cr, uid, ids)
519 res = super(hr_attendance,self).write(cr, uid, ids, vals, context=context)
520 if 'sheet_id' in context:
521 for attendance in self.browse(cr, uid, ids, context=context):
522 if context['sheet_id'] != attendance.sheet_id.id:
523 raise osv.except_osv(_('User Error!'), _('You cannot enter an attendance ' \
524 'date outside the current timesheet dates.'))
527 def _check(self, cr, uid, ids):
528 for att in self.browse(cr, uid, ids):
529 if att.sheet_id and att.sheet_id.state not in ('draft', 'new'):
530 raise osv.except_osv(_('Error!'), _('You cannot modify an entry in a confirmed timesheet'))
534 class hr_timesheet_sheet_sheet_day(osv.osv):
535 _name = "hr_timesheet_sheet.sheet.day"
536 _description = "Timesheets by Period"
540 'name': fields.date('Date', readonly=True),
541 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, select="1"),
542 'total_timesheet': fields.float('Total Timesheet', readonly=True),
543 'total_attendance': fields.float('Attendance', readonly=True),
544 'total_difference': fields.float('Difference', readonly=True),
547 'account.analytic.line': ['date', 'unit_amount'],
548 'hr.analytic.timesheet': ['line_id', 'sheet_id'],
549 'hr.attendance': ['action', 'name', 'sheet_id'],
553 cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
560 cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
567 SUM(total_timesheet) as total_timesheet,
568 CASE WHEN SUM(total_attendance) < 0
569 THEN (SUM(total_attendance) +
570 CASE WHEN current_date <> name
572 ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC') * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC')
575 ELSE SUM(total_attendance)
576 END /60 as total_attendance
581 l.date::date as name,
583 sum(l.unit_amount) as total_timesheet,
584 0.0 as total_attendance
586 hr_analytic_timesheet hrt
587 JOIN account_analytic_line l ON l.id = hrt.line_id
588 LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = hrt.sheet_id
589 group by l.date::date, s.id
593 a.name::date as name,
595 0.0 as total_timesheet,
596 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
599 LEFT JOIN hr_timesheet_sheet_sheet s
601 WHERE action in ('sign_in', 'sign_out')
602 group by a.name::date, s.id
604 GROUP BY name, sheet_id
609 class hr_timesheet_sheet_sheet_account(osv.osv):
610 _name = "hr_timesheet_sheet.sheet.account"
611 _description = "Timesheets by Period"
615 'name': fields.many2one('account.analytic.account', 'Project / Analytic Account', readonly=True),
616 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True),
617 'total': fields.float('Total Time', digits=(16,2), readonly=True),
618 'invoice_rate': fields.many2one('hr_timesheet_invoice.factor', 'Invoice rate', readonly=True),
622 'account.analytic.line': ['account_id', 'date', 'to_invoice', 'unit_amount', 'user_id'],
623 'hr.analytic.timesheet': ['line_id'],
624 'hr_timesheet_sheet.sheet': ['date_from', 'date_to', 'user_id'],
628 cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
631 l.account_id as name,
633 sum(l.unit_amount) as total,
634 l.to_invoice as invoice_rate
636 hr_analytic_timesheet hrt
637 left join (account_analytic_line l
638 LEFT JOIN hr_timesheet_sheet_sheet s
639 ON (s.date_to >= l.date
640 AND s.date_from <= l.date
641 AND s.user_id = l.user_id))
642 on (l.id = hrt.line_id)
643 group by l.account_id, s.id, l.to_invoice
649 class res_company(osv.osv):
650 _inherit = 'res.company'
652 'timesheet_range': fields.selection(
653 [('day','Day'),('week','Week'),('month','Month')], 'Timesheet range',
654 help="Periodicity on which you validate your timesheets."),
655 'timesheet_max_difference': fields.float('Timesheet allowed difference(Hours)',
656 help="Allowed difference in hours between the sign in/out and the timesheet " \
657 "computation for one sheet. Set this to 0 if you do not want any control."),
660 'timesheet_range': lambda *args: 'week',
661 'timesheet_max_difference': lambda *args: 0.0
664 class hr_employee(osv.osv):
669 _inherit = 'hr.employee'
670 _description = 'Employee'
672 def _timesheet_count(self, cr, uid, ids, field_name, arg, context=None):
673 Sheet = self.pool['hr_timesheet_sheet.sheet']
675 employee_id: Sheet.search_count(cr,uid, [('employee_id', '=', employee_id)], context=context)
676 for employee_id in ids
680 'timesheet_count': fields.function(_timesheet_count, type='integer', string='Timesheets'),
682 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: