Changes in project/project.py and hr/hr_view.xml
[odoo/odoo.git] / addons / project / project.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 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 from lxml import etree
23 from mx import DateTime
24 from mx.DateTime import now
25 import time
26 from tools.translate import _
27
28 from osv import fields, osv
29 from tools.translate import _
30
31 class project(osv.osv):
32     _name = "project.project"
33     _description = "Project"
34     _inherits = {'account.analytic.account':"category_id"}
35     def _complete_name(self, cr, uid, ids, name, args, context):
36         res = {}
37         for m in self.browse(cr, uid, ids, context=context):
38             res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
39         return res
40
41
42     def check_recursion(self, cursor, user, ids, parent=None):
43         return super(project, self).check_recursion(cursor, user, ids,
44                 parent=parent)
45
46     def onchange_partner_id(self, cr, uid, ids, part):
47         if not part:
48             return {'value':{'contact_id': False, 'pricelist_id': False}}
49         addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
50
51         pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
52         return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
53
54     def _progress_rate(self, cr, uid, ids, names, arg, context=None):
55         res = {}.fromkeys(ids, 0.0)
56         progress = {}
57         if not ids:
58             return res
59         ids2 = self.search(cr, uid, [('parent_id','child_of',ids)])
60         if ids2:
61             cr.execute('''SELECT
62                     project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
63                 FROM
64                     project_task
65                 WHERE
66                     project_id in ('''+','.join(map(str,ids2))+''') AND
67                     state<>'cancelled'
68                 GROUP BY
69                     project_id''')
70             progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3])), cr.fetchall()))
71         for project in self.browse(cr, uid, ids, context=context):
72             s = [0.0,0.0,0.0]
73             tocompute = [project]
74             while tocompute:
75                 p = tocompute.pop()
76                 tocompute += p.child_ids
77                 for i in range(3):
78                     s[i] += progress.get(p.id, (0.0,0.0,0.0))[i]
79             res[project.id] = {
80                 'planned_hours': s[0],
81                 'effective_hours': s[2],
82                 'total_hours': s[1],
83                 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
84             }
85         return res
86
87     def unlink(self, cr, uid, ids, *args, **kwargs):
88         for proj in self.browse(cr, uid, ids):
89             if proj.tasks:
90                 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
91         return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
92     _columns = {
93 #       'name': fields.char("Project Name", size=128, required=True),
94 #       'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=128),
95         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
96         'category_id': fields.many2one('account.analytic.account','Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc."),
97         'priority': fields.integer('Sequence'),
98 #       'manager': fields.many2one('res.users', 'Project Manager'),
99         'warn_manager': fields.boolean('Warn Manager', help="If you check this field, the project manager will receive a request each time a task is completed by his team."),
100         'resource_ids': fields.many2many('resource.resource', 'project_resource_rel', 'project_id', 'resource_id', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."),
101         'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
102 #        'parent_id': fields.many2one('project.project', 'Parent Project',\
103 #            help="If you have [?] in the name, it means there are no analytic account linked to project."),
104 #        'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
105         'planned_hours': fields.function(_progress_rate, multi="progress", method=True, string='Planned Time', help="Sum of planned hours of all tasks related to this project."),
106         'effective_hours': fields.function(_progress_rate, multi="progress", method=True, string='Time Spent', help="Sum of spent hours of all tasks related to this project."),
107         'total_hours': fields.function(_progress_rate, multi="progress", method=True, string='Total Time', help="Sum of total hours of all tasks related to this project."),
108         'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', help="Percent of tasks closed according to the total of tasks todo."),
109 #        'partner_id': fields.many2one('res.partner', 'Partner'),
110 #        'contact_id': fields.many2one('res.partner.address', 'Contact'),
111         'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer."),
112         'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed."),
113         'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed."),
114 #        'notes': fields.text('Notes', help="Internal description of the project."),
115         'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
116 #        'state': fields.selection([('template', 'Template'), ('open', 'Running'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True,
117 #                                  help='The project can be in either if the states \'Template\' and \'Running\'.\n If it is template then we can make projects based on the template projects. If its in \'Running\' state it is a normal project.\
118 #                                 \n If it is to be reviewed then the state is \'Pending\'.\n When the project is completed the state is set to \'Done\'.'),
119 #        'company_id': fields.many2one('res.company', 'Company'),
120      }
121
122     _defaults = {
123         'active': lambda *a: True,
124 #        'manager': lambda object,cr,uid,context: uid,
125         'priority': lambda *a: 1,
126 #        'state': lambda *a: 'open',
127 #        'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.project', context=c)
128     }
129
130 #    _order = "parent_id,priority,name"
131 #    _constraints = [
132 #        (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
133 #    ]
134
135     # toggle activity of projects, their sub projects and their tasks
136     def set_template(self, cr, uid, ids, context={}):
137         res = self.setActive(cr, uid, ids, value=False, context=context)
138         return res
139
140     def set_done(self, cr, uid, ids, context={}):
141         self.write(cr, uid, ids, {'state':'done'}, context=context)
142         return True
143
144     def set_cancel(self, cr, uid, ids, context={}):
145         self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
146         return True
147
148     def set_pending(self, cr, uid, ids, context={}):
149         self.write(cr, uid, ids, {'state':'pending'}, context=context)
150         return True
151
152     def set_open(self, cr, uid, ids, context={}):
153         self.write(cr, uid, ids, {'state':'open'}, context=context)
154         return True
155
156     def reset_project(self, cr, uid, ids, context={}):
157         res = self.setActive(cr, uid, ids,value=True, context=context)
158         return res
159
160     def copy(self, cr, uid, id, default={},context={}):
161         proj = self.browse(cr, uid, id, context=context)
162         default = default or {}
163         context['active_test'] = False
164         default['state'] = 'open'
165         if not default.get('name', False):
166             default['name'] = proj.name+_(' (copy)')
167         res = super(project, self).copy(cr, uid, id, default, context)
168         ids = self.search(cr, uid, [('parent_id','child_of', [res])])
169         cr.execute('update project_task set active=True where project_id in ('+','.join(map(str, ids))+')')
170         return res
171
172     def duplicate_template(self, cr, uid, ids,context={}):
173         for proj in self.browse(cr, uid, ids):
174             parent_id=context.get('parent_id',False)
175             new_id=self.pool.get('project.project').copy(cr, uid, proj.id,default={'name':proj.name+_(' (copy)'),'state':'open','parent_id':parent_id})
176             cr.execute('select id from project_task where project_id=%s', (proj.id,))
177             res = cr.fetchall()
178             for (tasks_id,) in res:
179                 self.pool.get('project.task').copy(cr, uid, tasks_id,default={'project_id':new_id,'active':True}, context=context)
180             cr.execute('select id from project_project where parent_id=%s', (proj.id,))
181             res = cr.fetchall()
182             project_ids = [x[0] for x in res]
183             for child in project_ids:
184                 self.duplicate_template(cr, uid, [child],context={'parent_id':new_id})
185
186         # TODO : Improve this to open the new project (using a wizard)
187
188         cr.commit()
189         raise osv.except_osv(_('Operation Done'), _('A new project has been created !\nWe suggest you to close this one and work on this new project.'))
190
191     # set active value for a project, its sub projects and its tasks
192     def setActive(self, cr, uid, ids, value=True, context={}):
193         for proj in self.browse(cr, uid, ids, context):
194             self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
195             cr.execute('select id from project_task where project_id=%s', (proj.id,))
196             tasks_id = [x[0] for x in cr.fetchall()]
197             if tasks_id:
198                 self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
199             cr.execute('select id from project_project where parent_id=%s', (proj.id,))
200             project_ids = [x[0] for x in cr.fetchall()]
201             for child in project_ids:
202                 self.setActive(cr, uid, [child], value, context)
203         return True
204 project()
205
206 class project_task_type(osv.osv):
207     _name = 'project.task.type'
208     _description = 'Project task type'
209     _columns = {
210         'name': fields.char('Type', required=True, size=64, translate=True),
211         'description': fields.text('Description'),
212     }
213 project_task_type()
214
215 class task(osv.osv):
216     _name = "project.task"
217     _description = "Tasks"
218     _date_name = "date_start"
219
220     def _str_get(self, task, level=0, border='***', context={}):
221         return border+' '+(task.user_id and task.user_id.name.upper() or '')+(level and (': L'+str(level)) or '')+(' - %.1fh / %.1fh'%(task.effective_hours or 0.0,task.planned_hours))+' '+border+'\n'+ \
222             border[0]+' '+(task.name or '')+'\n'+ \
223             (task.description or '')+'\n\n'
224
225     def _history_get(self, cr, uid, ids, name, args, context={}):
226         result = {}
227         for task in self.browse(cr, uid, ids, context=context):
228             result[task.id] = self._str_get(task, border='===')
229             t2 = task.parent_ids
230             level = 0
231             while t2:
232                 level -= 1
233                 result[task.id] = self._str_get(t2, level) + result[task.id]
234                 t2 = t2.parent_ids
235             t3 = map(lambda x: (x,1), task.child_ids)
236             while t3:
237                 t2 = t3.pop(0)
238                 result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
239                 t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
240         return result
241
242 # Compute: effective_hours, total_hours, progress
243     def _hours_get(self, cr, uid, ids, field_names, args, context):
244         task_set = ','.join(map(str, ids))
245         cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
246         hours = dict(cr.fetchall())
247         res = {}
248         for task in self.browse(cr, uid, ids, context=context):
249             res[task.id] = {}
250             res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
251             res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
252             if (task.remaining_hours + hours.get(task.id, 0.0)):
253                 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100),2)
254             else:
255                 res[task.id]['progress'] = 0.0
256             res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
257         return res
258
259     def onchange_planned(self, cr, uid, ids, planned, effective, date_start,occupation_rate=0.0):
260         result = {}
261         for res in self.browse(cr, uid, ids):
262             if date_start and planned:
263                 resource_id = self.pool.get('resource.resource').search(cr,uid,[('user_id','=',res.user_id.id)])
264                 resource_obj = self.pool.get('resource.resource').browse(cr,uid,resource_id)[0]
265                 print 'Resource Calendar::::',resource_obj.calendar_id.id
266                 d = mx.DateTime.strptime(date_start,'%Y-%m-%d %H:%M:%S')
267                 hrs = (planned)/(occupation_rate)
268                 print 'Hours::::',hrs
269                 work_times = self.pool.get('resource.calendar').interval_get(cr, uid, resource_obj.calendar_id.id or False, d, hrs or 0.0, resource_obj.id)
270                 print 'Date_end',work_times
271                 result['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
272                 print 'Date End',result['date_end']
273         result['remaining_hours'] = planned-effective
274         return {'value':result}
275
276
277     def _default_project(self, cr, uid, context={}):
278         if 'project_id' in context and context['project_id']:
279             return context['project_id']
280         return False
281
282     #_sql_constraints = [
283     #    ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
284     #]
285
286     def copy_data(self, cr, uid, id, default={},context={}):
287         default = default or {}
288         default['work_ids'] = []
289         return super(task, self).copy_data(cr, uid, id, default, context)
290
291         def _check_date(self,cr,uid,ids):
292             for res in self.browse(cr,uid,ids):
293                 if res.date_start and res.date_end:
294                     if res.date_start > res.date_end:
295                         return False
296                     return True
297
298     _columns = {
299         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it."),
300         'name': fields.char('Task summary', size=128, required=True),
301         'description': fields.text('Description'),
302         'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
303         'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
304         'type': fields.many2one('project.task.type', 'Type'),
305         'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
306                                   help='If the task is created the state \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
307                                   \n If the task is over, the states is set to \'Done\'.'),
308         'date_start': fields.datetime('Starting Date'),
309         'date_end': fields.datetime('Ending Date'),
310         'date_deadline': fields.datetime('Deadline'),
311         'date_close': fields.datetime('Date Closed', readonly=True),
312         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
313             help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
314         'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
315         'child_ids': fields.many2many('project.task', 'project_task_child_rel', 'task_id', 'child_id', 'Delegated Tasks'),
316         'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
317         'notes': fields.text('Notes'),
318         'occupation_rate': fields.float('Occupation Rate', help='The occupation rate fields indicates how much of his time a user is working on a task. A 100% occupation rate means the user works full time on the tasks. The ending date of a task is computed like this: Starting Date + Duration / Occupation Rate.'),
319         'planned_hours': fields.float('Planned Hours', required=True, help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
320         'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', store=True, help="Computed using the sum of the task work done."),
321         'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
322         'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
323         'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
324         'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as: Total Time - Estimated Time. It gives the difference of the time estimated by the project manager and the real time to close the task."),
325
326         'user_id': fields.many2one('res.users', 'Assigned to'),
327         'delegated_user_id': fields.related('child_ids','user_id',type='many2one', relation='res.users', string='Delegated To'),
328         'partner_id': fields.many2one('res.partner', 'Partner'),
329         'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
330         'manager_id': fields.related('project_id','manager', type='many2one', relation='res.users', string='Project Manager'),
331         'company_id': fields.many2one('res.company', 'Company'),
332     }
333     _defaults = {
334         'user_id': lambda obj,cr,uid,context: uid,
335         'state': lambda *a: 'draft',
336         'priority': lambda *a: '2',
337         'progress': lambda *a: 0,
338         'sequence': lambda *a: 10,
339         'active': lambda *a: True,
340         'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
341         'project_id': _default_project,
342         'occupation_rate':lambda *a: '1',
343         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
344     }
345     _order = "sequence, priority, date_deadline, id"
346
347     #
348     # Override view according to the company definition
349     #
350     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
351         obj_tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode_id
352         tm = obj_tm and obj_tm.name or 'Hours'
353
354         res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
355
356         if tm in ['Hours','Hour']:
357             return res
358
359         eview = etree.fromstring(res['arch'])
360
361         def _check_rec(eview):
362             if eview.attrib.get('widget','') == 'float_time':
363                 eview.set('widget','float')
364             for child in eview:
365                 _check_rec(child)
366             return True
367
368         _check_rec(eview)
369
370         res['arch'] = etree.tostring(eview)
371
372         for f in res['fields']:
373             if 'Hours' in res['fields'][f]['string']:
374                 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
375
376         return res
377
378     def do_close(self, cr, uid, ids, *args):
379         request = self.pool.get('res.request')
380         tasks = self.browse(cr, uid, ids)
381         for task in tasks:
382             project = task.project_id
383             if project:
384                 if project.warn_manager and project.user_id and (project.user_id.id != uid):
385                     request.create(cr, uid, {
386                         'name': _("Task '%s' closed") % task.name,
387                         'state': 'waiting',
388                         'act_from': uid,
389                         'act_to': project.user_id.id,
390                         'ref_partner_id': task.partner_id.id,
391                         'ref_doc1': 'project.task,%d'% (task.id,),
392                         'ref_doc2': 'project.project,%d'% (project.id,),
393                     })
394             self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
395             if task.parent_ids and task.parent_ids.state in ('pending','draft'):
396                 reopen = True
397                 for child in task.parent_ids.child_ids:
398                     if child.id != task.id and child.state not in ('done','cancelled'):
399                         reopen = False
400                 if reopen:
401                     self.do_reopen(cr, uid, [task.parent_ids.id])
402         return True
403
404     def do_reopen(self, cr, uid, ids, *args):
405         request = self.pool.get('res.request')
406         tasks = self.browse(cr, uid, ids)
407         for task in tasks:
408             project = task.project_id
409             if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
410                 request.create(cr, uid, {
411                     'name': _("Task '%s' set in progress") % task.name,
412                     'state': 'waiting',
413                     'act_from': uid,
414                     'act_to': project.user_id.id,
415                     'ref_partner_id': task.partner_id.id,
416                     'ref_doc1': 'project.task,%d' % task.id,
417                     'ref_doc2': 'project.project,%d' % project.id,
418                 })
419
420             self.write(cr, uid, [task.id], {'state': 'open'})
421         return True
422
423     def do_cancel(self, cr, uid, ids, *args):
424         request = self.pool.get('res.request')
425         tasks = self.browse(cr, uid, ids)
426         for task in tasks:
427             project = task.project_id
428             if project.warn_manager and project.user_id and (project.user_id.id != uid):
429                 request.create(cr, uid, {
430                     'name': _("Task '%s' cancelled") % task.name,
431                     'state': 'waiting',
432                     'act_from': uid,
433                     'act_to': project.user_id.id,
434                     'ref_partner_id': task.partner_id.id,
435                     'ref_doc1': 'project.task,%d' % task.id,
436                     'ref_doc2': 'project.project,%d' % project.id,
437                 })
438             self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
439         return True
440
441     def do_open(self, cr, uid, ids, *args):
442         tasks= self.browse(cr,uid,ids)
443         for t in tasks:
444             self.write(cr, uid, [t.id], {'state': 'open'})
445         return True
446
447     def do_draft(self, cr, uid, ids, *args):
448         self.write(cr, uid, ids, {'state': 'draft'})
449         return True
450
451
452     def do_pending(self, cr, uid, ids, *args):
453         self.write(cr, uid, ids, {'state': 'pending'})
454         return True
455
456
457 task()
458
459 class project_work(osv.osv):
460     _name = "project.task.work"
461     _description = "Task Work"
462     _columns = {
463         'name': fields.char('Work summary', size=128),
464         'date': fields.datetime('Date'),
465         'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
466         'hours': fields.float('Time Spent'),
467         'user_id': fields.many2one('res.users', 'Done by', required=True),
468         'company_id': fields.related('task_id','company_id',type='many2one',relation='res.company',string='Company',store=True)
469     }
470     _defaults = {
471         'user_id': lambda obj,cr,uid,context: uid,
472         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
473     }
474     _order = "date desc"
475     def create(self, cr, uid, vals, *args, **kwargs):
476         if 'hours' in vals and (not vals['hours']):
477             vals['hours'] = 0.00
478         if 'task_id' in vals:
479             cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
480         return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
481
482     def write(self, cr, uid, ids,vals,context={}):
483         if 'hours' in vals and (not vals['hours']):
484             vals['hours'] = 0.00
485         if 'hours' in vals:
486             for work in self.browse(cr, uid, ids, context):
487                 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (vals.get('hours',0.0), work.hours, work.task_id.id))
488         return super(project_work,self).write(cr, uid, ids, vals, context)
489
490     def unlink(self, cr, uid, ids, *args, **kwargs):
491         for work in self.browse(cr, uid, ids):
492             cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
493         return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
494 project_work()
495
496 class config_compute_remaining(osv.osv_memory):
497     _name='config.compute.remaining'
498     def _get_remaining(self,cr, uid, ctx):
499         if 'active_id' in ctx:
500             return self.pool.get('project.task').browse(cr,uid,ctx['active_id']).remaining_hours
501         return False
502
503     _columns = {
504         'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
505             }
506
507     _defaults = {
508         'remaining_hours': _get_remaining
509         }
510
511     def compute_hours(self, cr, uid, ids, context=None):
512         if 'active_id' in context:
513             remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
514             self.pool.get('project.task').write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
515         return {
516                 'type': 'ir.actions.act_window_close',
517          }
518 config_compute_remaining()
519
520 class message(osv.osv):
521     _name = "project.message"
522     _description = "Message"
523     _columns = {
524         'subject': fields.char('Subject', size=128),
525         'description': fields.char('Description', size =128),
526         'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
527         'date': fields.date('Date'),
528         'user_id': fields.many2one('res.users', 'User'),
529         }
530 message()
531
532 def _project_get(self, cr, uid, context={}):
533     obj = self.pool.get('project.project')
534     ids = obj.search(cr, uid, [])
535     res = obj.read(cr, uid, ids, ['id','name'], context)
536     res = [(str(r['id']),r['name']) for r in res]
537     return res
538
539 class users(osv.osv):
540     _inherit = 'res.users'
541     _description = "Users"
542     _columns = {
543         'context_project_id': fields.selection(_project_get, 'Project'),
544         }
545
546 users()
547
548 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: