HR_TIMESHEET_SHEET, CLIENT, KERNEL: improve hr_timesheet_sheet, add context to one2ma...
[odoo/odoo.git] / addons / hr_timesheet_sheet / hr_timesheet_sheet.py
1 ##############################################################################
2 #
3 # Copyright (c) 2006 TINY SPRL. (http://tiny.be) All Rights Reserved.
4 #
5 # $Id: hr_timesheet.py 5490 2007-01-29 16:05:51Z pinky $
6 #
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsability of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # garantees and support are strongly adviced to contract a Free Software
12 # Service Company
13 #
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
27 #
28 ##############################################################################
29
30 import time
31 from osv import fields
32 from osv import osv
33 import netsvc
34
35 from mx import DateTime
36
37 class one2many_mod2(fields.one2many):
38         def get(self, cr, obj, ids, name, user=None, offset=0, context={}, values={}):
39                 res = {}
40                 for id in ids:
41                         res[id] = []
42
43                 res5 = obj.read(cr, user, ids, ['date_current'], context)
44                 res6 = {}
45                 for r in res5:
46                         res6[r['id']] = r['date_current']
47
48                 for id in ids:
49                         dom = []
50                         if id in res6:
51                                 dom = [('name','>=',res6[id]+' 00:00:00'),('name','<=',res6[id]+' 23:59:59')]
52                         ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'=',id)]+dom, limit=self._limit)
53                         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
54                                 res[r[self._fields_id]].append( r['id'] )
55                 return res
56
57
58 class one2many_mod(fields.one2many):
59         def get(self, cr, obj, ids, name, user=None, offset=0, context={}, values={}):
60                 res = {}
61                 for id in ids:
62                         res[id] = []
63
64                 res5 = obj.read(cr, user, ids, ['date_current'], context)
65                 res6 = {}
66                 for r in res5:
67                         res6[r['id']] = r['date_current']
68
69                 for id in ids:
70                         dom = []
71                         if id in res6:
72                                 dom = [('date','=',res6[id])]
73                         ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'=',id)]+dom, limit=self._limit)
74                         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
75                                 res[r[self._fields_id]].append( r['id'] )
76                 return res
77
78 class hr_timesheet_sheet(osv.osv):
79         _name = "hr_timesheet_sheet.sheet"
80         _table = 'hr_timesheet_sheet_sheet'
81         _order = "id desc"
82         def _total_attendance_day(self, cr, uid, ids, name, args, context):
83                 result = {}
84                 for day in self.browse(cr, uid, ids, context):
85                         result[day.id] = 0.0
86                         obj = self.pool.get('hr_timesheet_sheet.sheet.day')
87                         ids = obj.search(cr, uid, [('sheet_id','=',day.id),('name','=',day.date_current)])
88                         if ids:
89                                 result[day.id] = obj.read(cr, uid, ids, ['total_attendance'])[0]['total_attendance'] or 0.0
90                 return result
91
92         def _total_timesheet_day(self, cr, uid, ids, name, args, context):
93                 result = {}
94                 for day in self.browse(cr, uid, ids, context):
95                         result[day.id] = 0.0
96                         obj = self.pool.get('hr_timesheet_sheet.sheet.day')
97                         ids = obj.search(cr, uid, [('sheet_id','=',day.id),('name','=',day.date_current)])
98                         if ids:
99                                 result[day.id] = obj.read(cr, uid, ids, ['total_timesheet'])[0]['total_timesheet'] or 0.0
100                 return result
101
102         def _total_difference_day(self, cr, uid, ids, name, args, context):
103                 result = {}
104                 for day in self.browse(cr, uid, ids, context):
105                         result[day.id] = 0.0
106                         obj = self.pool.get('hr_timesheet_sheet.sheet.day')
107                         ids = obj.search(cr, uid, [('sheet_id','=',day.id),('name','=',day.date_current)])
108                         if ids:
109                                 result[day.id] = obj.read(cr, uid, ids, ['total_difference'])[0]['total_difference'] or 0.0
110                 return result
111
112         def _total_attendance(self, cr, uid, ids, name, args, context):
113                 result = {}
114                 for day in self.browse(cr, uid, ids, context):
115                         result[day.id] = 0.0
116                         obj = self.pool.get('hr_timesheet_sheet.sheet.day')
117                         ids = obj.search(cr, uid, [('sheet_id','=',day.id)])
118                         for o in obj.browse(cr, uid, ids, context):
119                                 result[day.id] += o.total_attendance
120                 return result
121
122         def _total_timesheet(self, cr, uid, ids, name, args, context):
123                 result = {}
124                 for day in self.browse(cr, uid, ids, context):
125                         result[day.id] = 0.0
126                         obj = self.pool.get('hr_timesheet_sheet.sheet.day')
127                         ids = obj.search(cr, uid, [('sheet_id','=',day.id)])
128                         for o in obj.browse(cr, uid, ids, context):
129                                 result[day.id] += o.total_timesheet
130                 return result
131
132         def _total_difference(self, cr, uid, ids, name, args, context):
133                 result = {}
134                 for day in self.browse(cr, uid, ids, context):
135                         result[day.id] = 0.0
136                         obj = self.pool.get('hr_timesheet_sheet.sheet.day')
137                         ids = obj.search(cr, uid, [('sheet_id','=',day.id)])
138                         for o in obj.browse(cr, uid, ids, context):
139                                 result[day.id] += o.total_difference
140                 return result
141
142         def _state_attendance(self, cr, uid, ids, name, args, context):
143                 result = {}
144                 for day in self.browse(cr, uid, ids, context):
145                         emp_obj = self.pool.get('hr.employee')
146                         emp_ids = emp_obj.search(cr, uid, [('user_id', '=', day.user_id.id)])
147                         if emp_ids:
148                                 result[day.id] = emp_obj.browse(cr, uid, emp_ids[0], context).state
149                         else:
150                                 result[day.id] = 'none'
151                 return result
152
153         def button_confirm(self, cr, uid, ids, context):
154                 for sheet in self.browse(cr, uid, ids, context):
155                         di = sheet.user_id.company_id.timesheet_max_difference
156                         if (abs(sheet.total_difference) < di) or not di:
157                                 wf_service = netsvc.LocalService("workflow")
158                                 wf_service.trg_validate(uid, 'hr_timesheet_sheet.sheet', sheet.id, 'confirm', cr)
159                         else:
160                                 raise osv.except_osv('Warning !', 'Please verify that the total difference of the sheet is lower than %.2f !' %(di,))
161                 return True
162
163         def date_today(self, cr, uid, ids, context):
164                 for sheet in self.browse(cr, uid, ids, context):
165                         if DateTime.now() <= DateTime.strptime(sheet.date_from, '%Y-%m-%d'):
166                                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,})
167                         elif DateTime.now() >= DateTime.strptime(sheet.date_to, '%Y-%m-%d'):
168                                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,})
169                         else:
170                                 self.write(cr, uid, [sheet.id], {'date_current': time.strftime('%Y-%m-%d')})
171                 return True
172         def date_previous(self, cr, uid, ids, context):
173                 for sheet in self.browse(cr, uid, ids, context):
174                         if DateTime.strptime(sheet.date_current, '%Y-%m-%d') <= DateTime.strptime(sheet.date_from, '%Y-%m-%d'):
175                                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_from,})
176                         else:
177                                 self.write(cr, uid, [sheet.id], {
178                                         'date_current': (DateTime.strptime(sheet.date_current, '%Y-%m-%d') + DateTime.RelativeDateTime(days=-1)).strftime('%Y-%m-%d'),
179                                 })
180                 return True
181         def date_next(self, cr, uid, ids, context):
182                 for sheet in self.browse(cr, uid, ids, context):
183                         if DateTime.strptime(sheet.date_current, '%Y-%m-%d') >= DateTime.strptime(sheet.date_to, '%Y-%m-%d'):
184                                 self.write(cr, uid, [sheet.id], {'date_current': sheet.date_to,})
185                         else:
186                                 self.write(cr, uid, [sheet.id], {
187                                         'date_current': (DateTime.strptime(sheet.date_current, '%Y-%m-%d') + DateTime.RelativeDateTime(days=1)).strftime('%Y-%m-%d'),
188                                 })
189                 return True
190
191         def sign_in(self, cr, uid, ids, context):
192                 emp_obj = self.pool.get('hr.employee')
193                 emp_id = emp_obj.search(cr, uid, [('user_id', '=', uid)])
194                 context['sheet_id']=ids[0]
195                 success = emp_obj.sign_in(cr, uid, emp_id, context=context)
196                 return True
197
198         def sign_out(self, cr, uid, ids, context):
199                 emp_obj = self.pool.get('hr.employee')
200                 emp_id = emp_obj.search(cr, uid, [('user_id', '=', uid)])
201                 context['sheet_id']=ids[0]
202                 success = emp_obj.sign_out(cr, uid, emp_id, context=context)
203                 return True
204
205         _columns = {
206                 'name': fields.char('Description', size=64, select=1),
207                 'user_id': fields.many2one('res.users', 'User', required=True, select=1),
208                 'date_from': fields.date('Date from', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
209                 'date_to': fields.date('Date to', required=True, select=1, readonly=True, states={'new':[('readonly', False)]}),
210                 'date_current': fields.date('Current date', required=True),
211                 'timesheet_ids' : one2many_mod('hr.analytic.timesheet', 'sheet_id', 'Timesheets', domain=[('date','=',time.strftime('%Y-%m-%d'))], readonly=True, states={'draft':[('readonly',False)],'new':[('readonly',False)]}),
212                 'attendances_ids' : one2many_mod2('hr.attendance', 'sheet_id', 'Attendances', readonly=True, states={'draft':[('readonly',False)],'new':[('readonly',False)]}),
213                 'state' : fields.selection([('new', 'New'),('draft','Draft'),('confirm','Confirmed'),('done','Done')], 'state', select=True, required=True, readonly=True),
214                 'state_attendance' : fields.function(_state_attendance, method=True, type='selection', selection=[('absent', 'Absent'), ('present', 'Present'),('none','No employee defined')], string='Current state'),
215                 'total_attendance_day': fields.function(_total_attendance_day, method=True, string='Total Attendance'),
216                 'total_timesheet_day': fields.function(_total_timesheet_day, method=True, string='Total Timesheet'),
217                 'total_difference_day': fields.function(_total_difference_day, method=True, string='Difference'),
218                 'total_attendance': fields.function(_total_attendance, method=True, string='Total Attendance'),
219                 'total_timesheet': fields.function(_total_timesheet, method=True, string='Total Timesheet'),
220                 'total_difference': fields.function(_total_difference, method=True, string='Difference'),
221                 'period_ids': fields.one2many('hr_timesheet_sheet.sheet.day', 'sheet_id', 'Period', readonly=True),
222                 'account_ids': fields.one2many('hr_timesheet_sheet.sheet.account', 'sheet_id', 'Analytic accounts', readonly=True),
223         }
224         def _default_date_from(self,cr, uid, context={}):
225                 user = self.pool.get('res.users').browse(cr, uid, uid, context)
226                 r = user.company_id.timesheet_range
227                 if r=='month':
228                         return time.strftime('%Y-%m-01')
229                 elif r=='week':
230                         return (DateTime.now() + DateTime.RelativeDateTime(weekday=(DateTime.Monday,0))).strftime('%Y-%m-%d')
231                 elif r=='year':
232                         return time.strftime('%Y-01-01')
233                 return time.strftime('%Y-%m-%d')
234         def _default_date_to(self,cr, uid, context={}):
235                 user = self.pool.get('res.users').browse(cr, uid, uid, context)
236                 r = user.company_id.timesheet_range
237                 if r=='month':
238                         return (DateTime.now() + DateTime.RelativeDateTime(months=+1,day=1,days=-1)).strftime('%Y-%m-%d')
239                 elif r=='week':
240                         return (DateTime.now() + DateTime.RelativeDateTime(weekday=(DateTime.Sunday,0))).strftime('%Y-%m-%d')
241                 elif r=='year':
242                         return time.strftime('%Y-12-31')
243                 return time.strftime('%Y-%m-%d')
244         _defaults = {
245                 'user_id': lambda self,cr,uid,c: uid,
246                 'date_from' : _default_date_from,
247                 'date_current' : lambda *a: time.strftime('%Y-%m-%d'),
248                 'date_to' : _default_date_to,
249                 'state': lambda *a: 'new',
250         }
251         def create(self, cr, uid, vals, *args, **kwargs):
252                 if 'state' in vals and vals['state'] == 'new':
253                         vals['state']='draft'
254                 cr.execute('select id from hr_timesheet_sheet_sheet where (date_from < %s and %s < date_to) or (date_from < %s and %s <date_to) or (date_from >= %s and date_to <= %s)', (vals['date_from'], vals['date_from'], vals['date_to'], vals['date_to'], vals['date_from'], vals['date_to']))
255                 if cr.fetchall():
256                         raise osv.except_osv('Error !', 'A timesheet sheet already exists in this period')
257                 return super(hr_timesheet_sheet, self).create(cr, uid, vals, *args, **kwargs)
258 hr_timesheet_sheet()
259
260 def _get_current_sheet(self, cr, uid, context={}):
261         ts=self.pool.get('hr_timesheet_sheet.sheet')
262         ids = ts.search(cr, uid, [('user_id','=',uid),('state','=','draft'),('date_from','<=',time.strftime('%Y-%m-%d')), ('date_to','>=',time.strftime('%Y-%m-%d'))])
263         if ids:
264                 return ids[0]
265         return False
266
267
268 class hr_timesheet_line(osv.osv):
269         _inherit = "hr.analytic.timesheet"
270
271         def _get_default_date(self, cr, uid, context={}):
272                 if 'date' in context:
273                         return context['date']
274                 return time.strftime('%Y-%m-%d')
275
276         def _sheet_date(self, cr, uid, ids):
277                 timesheet_lines = self.browse(cr, uid, ids)
278                 for l in timesheet_lines:
279                         if l.date < l.sheet_id.date_from:
280                                 return False
281                         if l.date > l.sheet_id.date_to:
282                                 return False
283                 return True
284
285         _columns = {
286                 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', ondelete='set null', required=True, relate=True)
287         }
288         _defaults = {
289                 'sheet_id': _get_current_sheet,
290                 'date': _get_default_date,
291         }
292         _constraints = [(_sheet_date, 'Error: the timesheet line date must be in the sheet\'s dates', ['date'])]
293         def create(self, cr, uid, vals, *args, **kwargs):
294                 if 'sheet_id' in vals:
295                         ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, vals['sheet_id'])
296                         if ts.state<>'draft':
297                                 raise osv.except_osv('Error !', 'You can not modify an entry in a confirmed timesheet !')
298                 return super(hr_timesheet_line,self).create(cr, uid, vals, *args, **kwargs)
299         def unlink(self, cr, uid, ids, *args, **kwargs):
300                 self._check(cr, uid, ids)
301                 return super(hr_timesheet_line,self).unlink(cr, uid, ids,*args, **kwargs)
302         def write(self, cr, uid, ids, *args, **kwargs):
303                 self._check(cr, uid, ids)
304                 return super(hr_timesheet_line,self).write(cr, uid, ids,*args, **kwargs)
305         def _check(self, cr, uid, ids):
306                 for att in self.browse(cr, uid, ids):
307                         if att.sheet_id and att.sheet_id.state<>'draft':
308                                 raise osv.except_osv('Error !', 'You can not modify an entry in a confirmed timesheet !')
309                 return True
310 hr_timesheet_line()
311
312 class hr_attendance(osv.osv):
313         _inherit = "hr.attendance"
314
315         def _get_default_date(self, cr, uid, context={}):
316                 if 'name' in context:
317                         return context['name'] + time.strftime(' %H:%M:%S')
318                 return time.strftime('%Y-%m-%d %H:%M:%S')
319
320         def _sheet_date(self, cr, uid, ids):
321                 attendances = self.browse(cr, uid, ids)
322                 for att in attendances:
323                         if att.name < att.sheet_id.date_from:
324                                 return False
325                         if att.name > att.sheet_id.date_to:
326                                 return False
327                 return True
328
329         _columns = {
330                 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', ondelete='set null', required=True, relate=True)
331         }
332         _defaults = {
333                 'sheet_id': _get_current_sheet,
334                 'name': _get_default_date,
335         }
336         _constraints = [(_sheet_date, 'Error: the attendance date must be in the sheet\'s dates', ['name'])]
337         def create(self, cr, uid, vals, context={}):
338                 if 'sheet_id' in context:
339                         vals['sheet_id']=context['sheet_id']
340                 if 'sheet_id' in vals:
341                         ts = self.pool.get('hr_timesheet_sheet.sheet').browse(cr, uid, vals['sheet_id'])
342                         if ts.state<>'draft':
343                                 raise osv.except_osv('Error !', 'You can not modify an entry in a confirmed timesheet !')
344                 return super(hr_attendance,self).create(cr, uid, vals, context=context)
345         def unlink(self, cr, uid, ids, *args, **kwargs):
346                 self._check(cr, uid, ids)
347                 return super(hr_attendance,self).unlink(cr, uid, ids,*args, **kwargs)
348         def write(self, cr, uid, ids, vals, context={}):
349                 if 'sheet_id' in context:
350                         vals['sheet_id']=context['sheet_id']
351                 self._check(cr, uid, ids)
352                 return super(hr_attendance,self).write(cr, uid, ids, vals, context=context)
353         def _check(self, cr, uid, ids):
354                 for att in self.browse(cr, uid, ids):
355                         if att.sheet_id and att.sheet_id.state<>'draft':
356                                 raise osv.except_osv('Error !', 'You can not modify an entry in a confirmed timesheet !')
357                 return True
358 hr_attendance()
359
360 class hr_timesheet_sheet_sheet_day(osv.osv):
361         _name = "hr_timesheet_sheet.sheet.day"
362         _description = "Timesheets by period"
363         _auto = False
364         def _total_difference(self, cr, uid, ids, name, args, context):
365                 result = {}
366                 for day in self.browse(cr, uid, ids, context):
367                         result[day.id] = day.total_attendance-day.total_timesheet
368                 return result
369
370         def _total_attendance(self, cr, uid, ids, name, args, context):
371                 result = {}
372                 for day in self.browse(cr, uid, ids, context):
373                         cr.execute('select name,action from hr_attendance where name>=%s and name<=%s and sheet_id=%d order by name', (day.name, day.name+' 23:59:59', day.sheet_id))
374                         attendences = cr.dictfetchall()
375                         wh = 0
376                         if attendences and attendences[0]['action'] == 'sign_out':
377                                 attendences.insert(0, {'name': day.name+' 00:00:00', 'action':'sign_in'})
378                         if attendences and attendences[-1]['action'] == 'sign_in':
379                                 if day.name==time.strftime('%Y-%m-%d'):
380                                         attendences.append({'name': time.strftime('%Y-%m-%d %H:%M:%S'), 'action':'sign_out'})
381                                 else:
382                                         attendences.append({'name': day.name+' 23:59:59', 'action':'sign_out'})
383                         for att in attendences:
384                                 dt = DateTime.strptime(att['name'], '%Y-%m-%d %H:%M:%S')
385                                 if att['action'] == 'sign_out':
386                                         wh += (dt - ldt).hours
387                                 ldt = dt
388                         result[day.id] = round(wh,2)
389                 return result
390         _order='name'
391         _columns = {
392                 'name': fields.date('Date', readonly=True),
393                 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, select="1"),
394                 'total_timesheet': fields.float('Project Timesheet', readonly=True),
395                 'total_attendance': fields.function(_total_attendance, method=True, string='Attendance', readonly=True),
396                 'total_difference': fields.function(_total_difference, method=True, string='Difference', readonly=True),
397         }
398         def init(self, cr):
399                 cr.execute("""create or replace view hr_timesheet_sheet_sheet_day as
400                         (
401                                 select
402                                         min(hrt.id) as id,
403                                         l.date as name,
404                                         hrt.sheet_id as sheet_id,
405                                         sum(l.unit_amount) as total_timesheet
406                                 from
407                                         hr_analytic_timesheet hrt
408                                         left join account_analytic_line l on (l.id = hrt.line_id)
409                                 group by l.date, hrt.sheet_id
410                         ) union (
411                                 select
412                                         min(a.oid) as id,
413                                         a.name::date as name,
414                                         a.sheet_id as sheet_id,
415                                         0.0 as total_timesheet
416                                 from
417                                         hr_attendance a
418                                 where a.name::date not in (select distinct date from account_analytic_line)
419                                 group by a.name::date, a.sheet_id
420                         )""")
421 hr_timesheet_sheet_sheet_day()
422
423
424 class hr_timesheet_sheet_sheet_account(osv.osv):
425         _name = "hr_timesheet_sheet.sheet.account"
426         _description = "Timesheets by period"
427         _auto = False
428         _order='name'
429         _columns = {
430                 'name': fields.many2one('account.analytic.account', 'Analytic Account', readonly=True),
431                 'sheet_id': fields.many2one('hr_timesheet_sheet.sheet', 'Sheet', readonly=True, relate=True),
432                 'total': fields.float('Total Time', digits=(16,2), readonly=True),
433         }
434         def init(self, cr):
435                 cr.execute("""create or replace view hr_timesheet_sheet_sheet_account as (
436                         select
437                                 min(hrt.id) as id,
438                                 l.account_id as name,
439                                 hrt.sheet_id as sheet_id,
440                                 sum(l.unit_amount) as total
441                         from
442                                 hr_analytic_timesheet hrt
443                                 left join account_analytic_line l on (l.id = hrt.line_id)
444                         group by l.account_id, hrt.sheet_id
445                 )""")
446 hr_timesheet_sheet_sheet_account()
447
448 class res_company(osv.osv):
449         _inherit = 'res.company'
450         _columns = {
451                 'timesheet_range': fields.selection([('day','Day'),('week','Week'),('month','Month'),('year','Year')], 'Timeshet range', required=True),
452                 'timesheet_max_difference': fields.float('Timesheet allowed difference', help="Allowed difference between the sign in/out and the timesheet computation for one sheet. Set this to 0 if you do not want any control."),
453         }
454         _defaults = {
455                 'timesheet_range': lambda *args: 'month',
456                 'timesheet_max_difference': lambda *args: 0.0
457         }
458 res_company()