[MERGE] forward port of branch 8.0 up to e883193
[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', 'ir.needaction_mixin']
35     _table = 'hr_timesheet_sheet_sheet'
36     _order = "id desc"
37     _description = "Timesheet"
38
39     _track = {
40         'state': {
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',
43         },
44     }
45
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
49         """
50
51         res = {}
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,
57             })
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
62         return res
63
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')])
67
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.'))
70         return True
71
72     def copy(self, cr, uid, ids, *args, **argv):
73         raise osv.except_osv(_('Error!'), _('You cannot duplicate a timesheet.'))
74
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)
87
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
91             if not new_user_id:
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)'))
108         return res
109
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']
117                     else:
118                         name = self.pool['hr.attendance'].browse(cr, uid, att_tuple[1]).name
119                 else:
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))
124             else: 
125                 date_attendances.append((0, False, att_tuple))
126         date_attendances.sort()
127         return [att[2] for att in date_attendances]
128
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')
137             else:
138                 raise osv.except_osv(_('Warning!'), _('Please verify that the total difference of the sheet is lower than %.2f.') %(di,))
139         return True
140
141     def attendance_action_change(self, cr, uid, ids, context=None):
142         hr_employee = self.pool.get('hr.employee')
143         employee_ids = []
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)
147     
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']
151         return {
152             sheet_id: {
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)
155             }
156             for sheet_id in ids
157         }
158
159     _columns = {
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',
167             'Timesheet lines',
168             readonly=True, states={
169                 'draft': [('readonly', False)],
170                 'new': [('readonly', False)]}
171             ),
172         'attendances_ids' : fields.one2many('hr.attendance', 'sheet_id', 'Attendances'),
173         'state' : fields.selection([
174             ('new', 'New'),
175             ('draft','Open'),
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),
192     }
193
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'
197         if r=='month':
198             return time.strftime('%Y-%m-01')
199         elif r=='week':
200             return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d')
201         elif r=='year':
202             return time.strftime('%Y-01-01')
203         return time.strftime('%Y-%m-%d')
204
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'
208         if r=='month':
209             return (datetime.today() + relativedelta(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
210         elif r=='week':
211             return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')
212         elif r=='year':
213             return time.strftime('%Y-12-31')
214         return time.strftime('%Y-%m-%d')
215
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
219
220     _defaults = {
221         'date_from' : _default_date_from,
222         'date_to' : _default_date_to,
223         'state': 'new',
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)
226     }
227
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
231             if new_user_id:
232                 cr.execute('SELECT id \
233                     FROM hr_timesheet_sheet_sheet \
234                     WHERE (date_from <= %s and %s <= date_to) \
235                         AND user_id=%s \
236                         AND id <> %s',(sheet.date_to, sheet.date_from, new_user_id, sheet.id))
237                 if cr.fetchall():
238                     return False
239         return True
240
241
242     _constraints = [
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']),
244     ]
245
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)
249         return True
250
251     def name_get(self, cr, uid, ids, context=None):
252         if not ids:
253             return []
254         if isinstance(ids, (long, int)):
255             ids = [ids]
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')]
259
260     def unlink(self, cr, uid, ids, context=None):
261         sheets = self.read(cr, uid, ids, ['state','total_attendance'], context=context)
262         for sheet in sheets:
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)
268
269     def onchange_employee_id(self, cr, uid, ids, employee_id, context=None):
270         department_id =  False
271         user_id = False
272         if employee_id:
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,}}
277
278     # ------------------------------------------------
279     # OpenChatter methods and notifications
280     # ------------------------------------------------
281
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)
285         if not empids:
286             return False
287         dom = ['&', ('state', '=', 'confirm'), ('employee_id', 'in', empids)]
288         return dom
289
290
291 class account_analytic_line(osv.osv):
292     _inherit = "account.analytic.line"
293
294     def _get_default_date(self, cr, uid, context=None):
295         if context is None:
296             context = {}
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']:
303                 return res
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()
306         return res
307
308 class account_analytic_account(osv.osv):
309     _inherit = "account.analytic.account"
310     
311     def name_create(self, cr, uid, name, context=None):
312         if context is None:
313             context = {}
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]
319     
320 class hr_timesheet_line(osv.osv):
321     _inherit = "hr.analytic.timesheet"
322
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)],
330                 context=context)
331             if sheet_ids:
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]
334         return res
335
336     def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
337         ts_line_ids = []
338         for ts in self.browse(cr, uid, ids, context=context):
339             cr.execute("""
340                     SELECT l.id
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()])
351         return ts_line_ids
352
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)])
355         return ts_line_ids
356
357     _columns = {
358         'sheet_id': fields.function(_sheet, string='Sheet', select="1",
359             type='many2one', relation='hr_timesheet_sheet.sheet', ondelete="cascade",
360             store={
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),
364                   },
365             ),
366     }
367
368     def _check_sheet_state(self, cr, uid, ids, context=None):
369         if context is None:
370             context = {}
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'):
373                 return False
374         return True
375
376     _constraints = [
377         (_check_sheet_state, 'You cannot modify an entry in a Confirmed/Done timesheet !', ['state']),
378     ]
379
380     def unlink(self, cr, uid, ids, *args, **kwargs):
381         if isinstance(ids, (int, long)):
382             ids = [ids]
383         self._check(cr, uid, ids)
384         return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
385
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.'))
390         return True
391
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])
394
395
396
397 class hr_attendance(osv.osv):
398     _inherit = "hr.attendance"
399
400     def _get_default_date(self, cr, uid, context=None):
401         if context is None:
402             context = {}
403         if 'name' in context:
404             return context['name'] + time.strftime(' %H:%M:%S')
405         return time.strftime('%Y-%m-%d %H:%M:%S')
406
407     def _get_hr_timesheet_sheet(self, cr, uid, ids, context=None):
408         attendance_ids = []
409         for ts in self.browse(cr, uid, ids, context=context):
410             cr.execute("""
411                         SELECT a.id
412                           FROM hr_attendance a
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
425
426     def _get_attendance_employee_tz(self, cr, uid, employee_id, date, context=None):
427         """ Simulate timesheet in employee timezone
428
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
432         """
433         employee_obj = self.pool['hr.employee']
434
435         tz = False
436         if employee_id:
437             employee = employee_obj.browse(cr, uid, employee_id, context=context)
438             tz = employee.user_id.partner_id.tz
439
440         if not date:
441             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
442
443         att_tz = timezone(tz or 'utc')
444
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
453
454     def _get_current_sheet(self, cr, uid, employee_id, date=False, context=None):
455
456         sheet_obj = self.pool['hr_timesheet_sheet.sheet']
457         if not date:
458             date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
459
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
469
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,
475                     context=context)
476         return res
477
478     _columns = {
479         'sheet_id': fields.function(_sheet, string='Sheet',
480             type='many2one', relation='hr_timesheet_sheet.sheet',
481             store={
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),
484                   },
485             )
486     }
487     _defaults = {
488         'name': _get_default_date,
489     }
490
491     def create(self, cr, uid, vals, context=None):
492         if context is None:
493             context = {}
494
495         sheet_id = context.get('sheet_id') or self._get_current_sheet(cr, uid, vals.get('employee_id'), vals.get('name'), context=context)
496         if sheet_id:
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)
506
507     def unlink(self, cr, uid, ids, *args, **kwargs):
508         if isinstance(ids, (int, long)):
509             ids = [ids]
510         self._check(cr, uid, ids)
511         return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
512
513     def write(self, cr, uid, ids, vals, context=None):
514         if context is None:
515             context = {}
516         if isinstance(ids, (int, long)):
517             ids = [ids]
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.'))
525         return res
526
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'))
531         return True
532
533
534 class hr_timesheet_sheet_sheet_day(osv.osv):
535     _name = "hr_timesheet_sheet.sheet.day"
536     _description = "Timesheets by Period"
537     _auto = False
538     _order='name'
539     _columns = {
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),
545     }
546     _depends = {
547         'account.analytic.line': ['date', 'unit_amount'],
548         'hr.analytic.timesheet': ['line_id', 'sheet_id'],
549         'hr.attendance': ['action', 'name', 'sheet_id'],
550     }
551
552     def init(self, cr):
553         cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
554             SELECT
555                 id,
556                 name,
557                 sheet_id,
558                 total_timesheet,
559                 total_attendance,
560                 cast(round(cast(total_attendance - total_timesheet as Numeric),2) as Double Precision) AS total_difference
561             FROM
562                 ((
563                     SELECT
564                         MAX(id) as id,
565                         name,
566                         sheet_id,
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
571                                     THEN 1440
572                                     ELSE (EXTRACT(hour FROM current_time AT TIME ZONE 'UTC') * 60) + EXTRACT(minute FROM current_time AT TIME ZONE 'UTC')
573                                 END
574                                 )
575                             ELSE SUM(total_attendance)
576                         END /60  as total_attendance
577                     FROM
578                         ((
579                             select
580                                 min(hrt.id) as id,
581                                 l.date::date as name,
582                                 s.id as sheet_id,
583                                 sum(l.unit_amount) as total_timesheet,
584                                 0.0 as total_attendance
585                             from
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
590                         ) union (
591                             select
592                                 -min(a.id) as id,
593                                 a.name::date as name,
594                                 s.id as sheet_id,
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
597                             from
598                                 hr_attendance a
599                                 LEFT JOIN hr_timesheet_sheet_sheet s
600                                 ON s.id = a.sheet_id
601                             WHERE action in ('sign_in', 'sign_out')
602                             group by a.name::date, s.id
603                         )) AS foo
604                         GROUP BY name, sheet_id
605                 )) AS bar""")
606
607
608
609 class hr_timesheet_sheet_sheet_account(osv.osv):
610     _name = "hr_timesheet_sheet.sheet.account"
611     _description = "Timesheets by Period"
612     _auto = False
613     _order='name'
614     _columns = {
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),
619         }
620
621     _depends = {
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'],
625     }
626
627     def init(self, cr):
628         cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
629             select
630                 min(hrt.id) as id,
631                 l.account_id as name,
632                 s.id as sheet_id,
633                 sum(l.unit_amount) as total,
634                 l.to_invoice as invoice_rate
635             from
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
644         )""")
645
646
647
648
649 class res_company(osv.osv):
650     _inherit = 'res.company'
651     _columns = {
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."),
658     }
659     _defaults = {
660         'timesheet_range': lambda *args: 'week',
661         'timesheet_max_difference': lambda *args: 0.0
662     }
663
664 class hr_employee(osv.osv):
665     '''
666     Employee
667     '''
668
669     _inherit = 'hr.employee'
670     _description = 'Employee'
671
672     def _timesheet_count(self, cr, uid, ids, field_name, arg, context=None):
673         Sheet = self.pool['hr_timesheet_sheet.sheet']
674         return {
675             employee_id: Sheet.search_count(cr,uid, [('employee_id', '=', employee_id)], context=context)
676             for employee_id in ids
677         }
678
679     _columns = {
680         'timesheet_count': fields.function(_timesheet_count, type='integer', string='Timesheets'),
681     }
682 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: