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