1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from lxml import etree
26 from tools.translate import _
27 from osv import fields, osv
28 from tools.translate import _
30 class project_task_type(osv.osv):
31 _name = 'project.task.type'
32 _description = 'Project task type'
34 'name': fields.char('Type', required=True, size=64, translate=True),
35 'description': fields.text('Description'),
36 'sequence': fields.integer('Sequence'),
41 'sequence': lambda *args: 1
45 class project(osv.osv):
46 _name = "project.project"
47 _description = "Project"
48 _inherits = {'account.analytic.account':"category_id"}
49 def _complete_name(self, cr, uid, ids, name, args, context):
51 for m in self.browse(cr, uid, ids, context=context):
52 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
56 def check_recursion(self, cursor, user, ids, parent=None):
57 return super(project, self).check_recursion(cursor, user, ids,
60 def onchange_partner_id(self, cr, uid, ids, part):
62 return {'value':{'contact_id': False, 'pricelist_id': False}}
63 addr = self.pool.get('res.partner').address_get(cr, uid, [part], ['contact'])
65 pricelist = self.pool.get('res.partner').browse(cr, uid, part).property_product_pricelist.id
66 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist}}
68 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
69 res = {}.fromkeys(ids, 0.0)
73 ids2 = self.search(cr, uid, [('parent_id','child_of',ids)])
76 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
80 project_id in ('''+','.join(map(str,ids2))+''') AND
84 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3])), cr.fetchall()))
85 for project in self.browse(cr, uid, ids, context=context):
90 tocompute += p.child_ids
92 s[i] += progress.get(p.id, (0.0,0.0,0.0))[i]
94 'planned_hours': s[0],
95 'effective_hours': s[2],
97 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
101 def unlink(self, cr, uid, ids, *args, **kwargs):
102 for proj in self.browse(cr, uid, ids):
104 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
105 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
107 # 'name': fields.char("Project Name", size=128, required=True),
108 # 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=128),
109 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
110 '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."),
111 'priority': fields.integer('Sequence'),
112 # 'manager': fields.many2one('res.users', 'Project Manager'),
113 '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."),
114 '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."),
115 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
116 # 'parent_id': fields.many2one('project.project', 'Parent Project',\
117 # help="If you have [?] in the name, it means there are no analytic account linked to project."),
118 # 'child_id': fields.one2many('project.project', 'parent_id', 'Subproject'),
119 '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."),
120 '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."),
121 '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."),
122 '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."),
123 # 'partner_id': fields.many2one('res.partner', 'Partner'),
124 # 'contact_id': fields.many2one('res.partner.address', 'Contact'),
125 '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."),
126 '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."),
127 '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."),
128 # 'notes': fields.text('Notes', help="Internal description of the project."),
129 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
130 # 'state': fields.selection([('template', 'Template'), ('open', 'Running'), ('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', required=True, readonly=True,
131 # 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.\
132 # \n If it is to be reviewed then the state is \'Pending\'.\n When the project is completed the state is set to \'Done\'.'),
133 # 'company_id': fields.many2one('res.company', 'Company'),
134 # 'timesheet_id': fields.many2one('hr.timesheet.group', 'Working Time', help="Timetable working hours to adjust the gantt diagram report"),
135 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Allowed Task Types'),
139 'active': lambda *a: True,
140 # 'manager': lambda object,cr,uid,context: uid,
141 'priority': lambda *a: 1,
142 # 'state': lambda *a: 'open',
143 # 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.project', context=c)
145 def _check_dates(self, cr, uid, ids):
146 leave = self.read(cr, uid, ids[0],['date_start','date'])
147 if leave['date_start'] and leave['date']:
148 if leave['date_start'] > leave['date']:
153 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
156 # _order = "parent_id,priority,name"
158 # (check_recursion, 'Error ! You can not create recursive projects.', ['parent_id'])
161 # toggle activity of projects, their sub projects and their tasks
162 def set_template(self, cr, uid, ids, context={}):
163 res = self.setActive(cr, uid, ids, value=False, context=context)
166 def set_done(self, cr, uid, ids, context={}):
167 self.write(cr, uid, ids, {'state':'close'}, context=context)
170 def set_cancel(self, cr, uid, ids, context={}):
171 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
174 def set_pending(self, cr, uid, ids, context={}):
175 self.write(cr, uid, ids, {'state':'pending'}, context=context)
178 def set_open(self, cr, uid, ids, context={}):
179 self.write(cr, uid, ids, {'state':'open'}, context=context)
182 def reset_project(self, cr, uid, ids, context={}):
183 res = self.setActive(cr, uid, ids,value=True, context=context)
186 def copy(self, cr, uid, id, default={},context={}):
187 proj = self.browse(cr, uid, id, context=context)
188 default = default or {}
189 context['active_test'] = False
190 default['state'] = 'open'
191 if not default.get('name', False):
192 default['name'] = proj.name+_(' (copy)')
193 res = super(project, self).copy(cr, uid, id, default, context)
194 ids = self.search(cr, uid, [('parent_id','child_of', [res])])
196 cr.execute('update project_task set active=True where project_id in ('+','.join(map(str, ids))+')')
199 def duplicate_template(self, cr, uid, ids,context={}):
201 for proj in self.browse(cr, uid, ids):
202 parent_id = context.get('parent_id',False)
203 new_id = self.pool.get('project.project').copy(cr, uid, proj.id, default = {
204 'name': proj.name +_(' (copy)'),
206 'parent_id':parent_id})
207 result.append(new_id)
208 cr.execute('select id from project_task where project_id=%s', (proj.id,))
210 for (tasks_id,) in res:
211 self.pool.get('project.task').copy(cr, uid, tasks_id, default = {
212 'project_id': new_id,
213 'active':True}, context=context)
214 child_ids = self.search(cr, uid, [('parent_id','=', proj.id)])
216 self.duplicate_template(cr, uid, child_ids, context={'parent_id':new_id})
219 # set active value for a project, its sub projects and its tasks
220 def setActive(self, cr, uid, ids, value=True, context={}):
221 for proj in self.browse(cr, uid, ids, context):
222 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
223 cr.execute('select id from project_task where project_id=%s', (proj.id,))
224 tasks_id = [x[0] for x in cr.fetchall()]
226 self.pool.get('project.task').write(cr, uid, tasks_id, {'active': value}, context)
227 child_ids = self.search(cr, uid, [('parent_id','=', proj.id)])
229 self.setActive(cr, uid, child_ids, value, context)
234 _name = "project.task"
235 _description = "Tasks"
236 _date_name = "date_start"
238 # def compute_date(self,cr,uid):
239 # project_id = self.pool.get('project.project').search(cr,uid,[])
240 # for i in range(len(project_id)):
241 # task_ids = self.pool.get('project.task').search(cr,uid,[('project_id','=',project_id[i])])
243 # task_obj = self.pool.get('project.task').browse(cr,uid,task_ids)
244 # task_1 = task_obj[0]
245 # task_1.date_start = self.pool.get('project.project').browse(cr,uid,project_id[i]).date_start
246 # dt = mx.DateTime.strptime(task_1.date_start,"%Y-%m-%d").strftime("%Y-%m-%d")
248 # title = "New Project"
253 # effort = task_1.planned_hours
255 ## project_1 = BalancedProject(Project_1)
256 ## for t in project_1:
257 ## print 'details:::',t.indent_name(), t.start, t.end, t.effort
260 def _str_get(self, task, level=0, border='***', context={}):
261 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'+ \
262 border[0]+' '+(task.name or '')+'\n'+ \
263 (task.description or '')+'\n\n'
265 def _history_get(self, cr, uid, ids, name, args, context={}):
267 for task in self.browse(cr, uid, ids, context=context):
268 result[task.id] = self._str_get(task, border='===')
273 result[task.id] = self._str_get(t2, level) + result[task.id]
275 t3 = map(lambda x: (x,1), task.child_ids)
278 result[task.id] = result[task.id] + self._str_get(t2[0], t2[1])
279 t3 += map(lambda x: (x,t2[1]+1), t2[0].child_ids)
282 # Compute: effective_hours, total_hours, progress
283 def _hours_get(self, cr, uid, ids, field_names, args, context):
284 task_set = ','.join(map(str, ids))
285 cr.execute(("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id in (%s) GROUP BY task_id") % (task_set,))
286 hours = dict(cr.fetchall())
288 for task in self.browse(cr, uid, ids, context=context):
290 res[task.id]['effective_hours'] = hours.get(task.id, 0.0)
291 res[task.id]['total_hours'] = task.remaining_hours + hours.get(task.id, 0.0)
292 if (task.remaining_hours + hours.get(task.id, 0.0)):
293 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 100),2)
295 res[task.id]['progress'] = 0.0
296 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
299 def onchange_planned(self, cr, uid, ids, planned, effective, date_start,occupation_rate=0.0):
301 for res in self.browse(cr, uid, ids):
302 if date_start and planned:
303 resource_id = self.pool.get('resource.resource').search(cr,uid,[('user_id','=',res.user_id.id)])
304 resource_obj = self.pool.get('resource.resource').browse(cr,uid,resource_id)[0]
305 d = mx.DateTime.strptime(date_start,'%Y-%m-%d %H:%M:%S')
306 hrs = (planned)/(occupation_rate)
307 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)
308 result['date_end'] = work_times[-1][1].strftime('%Y-%m-%d %H:%M:%S')
309 result['remaining_hours'] = planned-effective
310 return {'value':result}
313 def _default_project(self, cr, uid, context={}):
314 if 'project_id' in context and context['project_id']:
315 return context['project_id']
318 #_sql_constraints = [
319 # ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
322 def copy_data(self, cr, uid, id, default={},context={}):
323 default = default or {}
324 default['work_ids'] = []
325 return super(task, self).copy_data(cr, uid, id, default, context)
327 def _check_date(self,cr,uid,ids):
328 for res in self.browse(cr,uid,ids):
329 if res.date_start and res.date_end:
330 if res.date_start > res.date_end:
335 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it."),
336 'name': fields.char('Task Summary', size=128, required=True),
337 'description': fields.text('Description'),
338 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
339 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
340 'type': fields.many2one('project.task.type', 'Type', readonly=True),
341 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
342 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.\
343 \n If the task is over, the states is set to \'Done\'.'),
344 'date_start': fields.datetime('Starting Date'),
345 'date_end': fields.datetime('Ending Date'),
346 'date_deadline': fields.datetime('Deadline'),
347 'date_close': fields.datetime('Date Closed', readonly=True),
348 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
349 help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
350 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
351 'child_ids': fields.many2many('project.task', 'project_task_child_rel', 'task_id', 'child_id', 'Delegated Tasks'),
352 'history': fields.function(_history_get, method=True, string="Task Details", type="text"),
353 'notes': fields.text('Notes'),
354 '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.'),
355 '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.'),
356 '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."),
357 'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
358 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
359 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
360 '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."),
362 'user_id': fields.many2one('res.users', 'Assigned to'),
363 'delegated_user_id': fields.related('child_ids','user_id',type='many2one', relation='res.users', string='Delegated To'),
364 'partner_id': fields.many2one('res.partner', 'Partner'),
365 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
366 'manager_id': fields.related('project_id','category_id','user_id', type='many2one', relation='res.users', string='Project Manager'),
367 'company_id': fields.many2one('res.company', 'Company'),
370 'user_id': lambda obj,cr,uid,context: uid,
371 'state': lambda *a: 'draft',
372 'priority': lambda *a: '2',
373 'progress': lambda *a: 0,
374 'sequence': lambda *a: 10,
375 'active': lambda *a: True,
376 'date_start': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
377 'project_id': _default_project,
378 'occupation_rate':lambda *a: '1',
379 'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
381 _order = "sequence, priority, date_deadline, id"
384 # Override view according to the company definition
386 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
387 obj_tm = self.pool.get('res.users').browse(cr, uid, uid, context).company_id.project_time_mode_id
388 tm = obj_tm and obj_tm.name or 'Hours'
390 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
392 if tm in ['Hours','Hour']:
395 eview = etree.fromstring(res['arch'])
397 def _check_rec(eview):
398 if eview.attrib.get('widget','') == 'float_time':
399 eview.set('widget','float')
406 res['arch'] = etree.tostring(eview)
408 for f in res['fields']:
409 if 'Hours' in res['fields'][f]['string']:
410 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
414 def do_close(self, cr, uid, ids, *args):
415 request = self.pool.get('res.request')
416 tasks = self.browse(cr, uid, ids)
418 project = task.project_id
420 if project.warn_manager and project.user_id and (project.user_id.id != uid):
421 request.create(cr, uid, {
422 'name': _("Task '%s' closed") % task.name,
425 'act_to': project.user_id.id,
426 'ref_partner_id': task.partner_id.id,
427 'ref_doc1': 'project.task,%d'% (task.id,),
428 'ref_doc2': 'project.project,%d'% (project.id,),
430 self.write(cr, uid, [task.id], {'state': 'done', 'date_close':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
431 if task.parent_ids and task.parent_ids.state in ('pending','draft'):
433 for child in task.parent_ids.child_ids:
434 if child.id != task.id and child.state not in ('done','cancelled'):
437 self.do_reopen(cr, uid, [task.parent_ids.id])
440 def do_reopen(self, cr, uid, ids, *args):
441 request = self.pool.get('res.request')
442 tasks = self.browse(cr, uid, ids)
444 project = task.project_id
445 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
446 request.create(cr, uid, {
447 'name': _("Task '%s' set in progress") % task.name,
450 'act_to': project.user_id.id,
451 'ref_partner_id': task.partner_id.id,
452 'ref_doc1': 'project.task,%d' % task.id,
453 'ref_doc2': 'project.project,%d' % project.id,
456 self.write(cr, uid, [task.id], {'state': 'open'})
459 def do_cancel(self, cr, uid, ids, *args):
460 request = self.pool.get('res.request')
461 tasks = self.browse(cr, uid, ids)
463 project = task.project_id
464 if project.warn_manager and project.user_id and (project.user_id.id != uid):
465 request.create(cr, uid, {
466 'name': _("Task '%s' cancelled") % task.name,
469 'act_to': project.user_id.id,
470 'ref_partner_id': task.partner_id.id,
471 'ref_doc1': 'project.task,%d' % task.id,
472 'ref_doc2': 'project.project,%d' % project.id,
474 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
477 def do_open(self, cr, uid, ids, *args):
478 tasks= self.browse(cr,uid,ids)
480 self.write(cr, uid, [t.id], {'state': 'open'})
483 def do_draft(self, cr, uid, ids, *args):
484 self.write(cr, uid, ids, {'state': 'draft'})
488 def do_pending(self, cr, uid, ids, *args):
489 self.write(cr, uid, ids, {'state': 'pending'})
492 def next_type(self, cr, uid, ids, *args):
493 for typ in self.browse(cr, uid, ids):
495 types = map(lambda x:x.id, typ.project_id.type_ids or [])
498 self.write(cr, uid, typ.id, {'type': types[0]})
499 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
500 index = types.index(typeid)
501 self.write(cr, uid, typ.id, {'type': types[index+1]})
504 def prev_type(self, cr, uid, ids, *args):
505 for typ in self.browse(cr, uid, ids):
507 types = map(lambda x:x.id, typ.project_id.type_ids)
509 if typeid and typeid in types and types.index(typeid) != 0 :
510 index = types.index(typeid)
511 self.write(cr, uid, typ.id, {'type': types[index-1]})
516 class project_work(osv.osv):
517 _name = "project.task.work"
518 _description = "Task Work"
520 'name': fields.char('Work summary', size=128),
521 'date': fields.datetime('Date'),
522 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
523 'hours': fields.float('Time Spent'),
524 'user_id': fields.many2one('res.users', 'Done by', required=True),
525 'company_id': fields.related('task_id','company_id',type='many2one',relation='res.company',string='Company',store=True)
528 'user_id': lambda obj,cr,uid,context: uid,
529 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
532 def create(self, cr, uid, vals, *args, **kwargs):
533 if 'hours' in vals and (not vals['hours']):
535 if 'task_id' in vals:
536 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
537 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
539 def write(self, cr, uid, ids,vals,context={}):
540 if 'hours' in vals and (not vals['hours']):
543 for work in self.browse(cr, uid, ids, context):
544 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))
545 return super(project_work,self).write(cr, uid, ids, vals, context)
547 def unlink(self, cr, uid, ids, *args, **kwargs):
548 for work in self.browse(cr, uid, ids):
549 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
550 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
553 class config_compute_remaining(osv.osv_memory):
554 _name='config.compute.remaining'
555 def _get_remaining(self,cr, uid, ctx):
556 if 'active_id' in ctx:
557 return self.pool.get('project.task').browse(cr,uid,ctx['active_id']).remaining_hours
561 'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
565 'remaining_hours': _get_remaining
568 def compute_hours(self, cr, uid, ids, context=None):
569 if 'active_id' in context:
570 remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
571 self.pool.get('project.task').write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
573 'type': 'ir.actions.act_window_close',
575 config_compute_remaining()
577 class message(osv.osv):
578 _name = "project.message"
579 _description = "Message"
581 'subject': fields.char('Subject', size=128),
582 'description': fields.char('Description', size =128),
583 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
584 'date': fields.date('Date'),
585 'user_id': fields.many2one('res.users', 'User'),
589 def _project_get(self, cr, uid, context={}):
591 ids = self.pool.get('project.project').search(cr, uid, [])
592 res = self.pool.get('project.project').read(cr, uid, ids, ['id','name'], context)
593 res = [(str(r['id']),r['name']) for r in res]
595 cr.execute("""SELECT project.id,account.name FROM project_project project
596 LEFT JOIN account_analytic_account account ON account.id = project.category_id
597 WHERE (account.user_id = %s) OR project.id IN (SELECT project_id FROM project_resource_rel
598 WHERE resource_id IN (SELECT id FROM resource_resource
599 WHERE (user_id= %s)))"""%(uid, uid))
601 res = [(str(r[0]),r[1]) for r in res]
604 class users(osv.osv):
605 _inherit = 'res.users'
606 _description = "Users"
608 'context_project_id': fields.selection(_project_get, 'Project'),
612 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: