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