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
24 from datetime import date, datetime
25 from tools.translate import _
26 from osv import fields, osv
28 class project_task_type(osv.osv):
29 _name = 'project.task.type'
30 _description = 'Task Stage'
32 'name': fields.char('Stage Name', required=True, size=64, translate=True),
33 'description': fields.text('Description'),
34 'sequence': fields.integer('Sequence'),
44 class project(osv.osv):
45 _name = "project.project"
46 _description = "Project"
47 _inherits = {'account.analytic.account':"category_id"}
49 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
51 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
52 if context and context.has_key('user_prefence') and context['user_prefence']:
53 cr.execute("""SELECT project.id FROM project_project project
54 LEFT JOIN account_analytic_account account ON account.id = project.category_id
55 LEFT JOIN project_user_rel rel ON rel.project_id = project.category_id
56 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
58 return [(r[0]) for r in res]
59 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
60 context=context, count=count)
62 def _complete_name(self, cr, uid, ids, name, args, context=None):
64 for m in self.browse(cr, uid, ids, context=context):
65 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
68 def check_recursion(self, cursor, user, ids, parent=None):
69 return super(project, self).check_recursion(cursor, user, ids,
72 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
73 partner_obj = self.pool.get('res.partner')
75 return {'value':{'contact_id': False, 'pricelist_id': False}}
76 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
77 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
78 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
79 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
81 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
82 res = {}.fromkeys(ids, 0.0)
86 ids2 = self.search(cr, uid, [('parent_id','child_of',ids)], context=context)
89 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
96 project_id''',(tuple(ids2),))
97 progress = dict(map(lambda x: (x[0], (x[1], x[2], x[3])), cr.fetchall()))
98 for project in self.browse(cr, uid, ids, context=context):
100 tocompute = [project]
103 tocompute += p.child_ids
105 s[i] += progress.get(p.id, (0.0, 0.0, 0.0))[i]
107 'planned_hours': s[0],
108 'effective_hours': s[2],
110 'progress_rate': s[1] and (100.0 * s[2] / s[1]) or 0.0
114 def unlink(self, cr, uid, ids, *args, **kwargs):
115 for proj in self.browse(cr, uid, ids):
117 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
118 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
121 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
122 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
123 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
124 '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."),
125 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
126 '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."),
127 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's member. Not used in any computation, just for information purpose."),
128 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
129 '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."),
130 '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."),
131 '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."),
132 '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."),
133 '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."),
134 '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."),
135 '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."),
136 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages'),
137 'project_escalation_id': fields.many2one('project.project','Project Escalation', help='If any issue is escalated from the current Project, it will be listed under the project selected here.'),
146 'warn_manager': True,
149 def _check_dates(self, cr, uid, ids):
150 leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
151 if leave['date_start'] and leave['date']:
152 if leave['date_start'] > leave['date']:
156 def _check_escalation(self, cr, uid, ids):
157 project_obj = self.browse(cr, uid, ids[0])
158 if project_obj.project_escalation_id:
159 if project_obj.project_escalation_id.id == project_obj.id:
164 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date']),
165 (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id'])
168 def set_template(self, cr, uid, ids, context=None):
169 res = self.setActive(cr, uid, ids, value=False, context=context)
172 def set_done(self, cr, uid, ids, context=None):
173 task_obj = self.pool.get('project.task')
174 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
175 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
176 self.write(cr, uid, ids, {'state':'close'}, context=context)
177 for (id, name) in self.name_get(cr, uid, ids):
178 message = _('Project ') + " '" + name + "' "+ _("is Closed.")
179 self.log(cr, uid, id, message)
182 def set_cancel(self, cr, uid, ids, context=None):
183 task_obj = self.pool.get('project.task')
184 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
185 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
189 def set_pending(self, cr, uid, ids, context=None):
190 self.write(cr, uid, ids, {'state':'pending'}, context=context)
193 def set_open(self, cr, uid, ids, context=None):
194 self.write(cr, uid, ids, {'state':'open'}, context=context)
197 def reset_project(self, cr, uid, ids, context=None):
198 res = self.setActive(cr, uid, ids, value=True, context=context)
199 for (id, name) in self.name_get(cr, uid, ids):
200 message = _('Project ') + " '" + name + "' "+ _("is Open.")
201 self.log(cr, uid, id, message)
204 def copy(self, cr, uid, id, default={}, context=None):
208 task_obj = self.pool.get('project.task')
209 proj = self.browse(cr, uid, id, context=context)
210 default = default or {}
211 context['active_test'] = False
212 default['state'] = 'open'
213 if not default.get('name', False):
214 default['name'] = proj.name+_(' (copy)')
215 res = super(project, self).copy(cr, uid, id, default, context)
217 task_ids = task_obj.search(cr, uid, [('project_id','=', res), ('active','=',False)])
218 tasks = task_obj.browse(cr, uid, task_ids)
223 ds = date(*time.strptime(task.date_start,'%Y-%m-%d %H:%M:%S')[:3])
224 if task.date_deadline:
225 dd = date(*time.strptime(task.date_deadline,'%Y-%m-%d')[:3])
227 date_deadline = (datetime.now()+diff).strftime('%Y-%m-%d %H:%M:%S')
228 task_obj.write(cr, uid, task.id, {'active':True,
229 'date_start':time.strftime('%Y-%m-%d %H:%M:%S'),
230 'date_deadline':date_deadline,
231 'date_end':date_end})
233 ids = self.search(cr, uid, [('parent_id','child_of', [res])])
236 def duplicate_template(self, cr, uid, ids, context=None):
239 project_obj = self.pool.get('project.project')
240 data_obj = self.pool.get('ir.model.data')
241 task_obj = self.pool.get('project.task')
243 for proj in self.browse(cr, uid, ids, context=context):
244 parent_id = context.get('parent_id', False) # check me where to pass context for parent id ??
245 context.update({'analytic_project_copy': True})
246 new_id = project_obj.copy(cr, uid, proj.id, default = {
247 'name': proj.name +_(' (copy)'),
249 'parent_id':parent_id}, context=context)
250 result.append(new_id)
251 cr.execute('select id from project_task where project_id=%s', (proj.id,))
253 child_ids = self.search(cr, uid, [('parent_id','=', proj.category_id.id)], context=context)
254 parent_id = self.read(cr, uid, new_id, ['category_id'])['category_id'][0]
256 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
258 if result and len(result):
260 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
261 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
262 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_list')
263 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
264 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
265 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
267 'name': _('Projects'),
269 'view_mode': 'form,tree',
270 'res_model': 'project.project',
273 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
274 'type': 'ir.actions.act_window',
275 'search_view_id': search_view['res_id'],
279 # set active value for a project, its sub projects and its tasks
280 def setActive(self, cr, uid, ids, value=True, context=None):
281 task_obj = self.pool.get('project.task')
282 for proj in self.browse(cr, uid, ids, context=None):
283 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
284 cr.execute('select id from project_task where project_id=%s', (proj.id,))
285 tasks_id = [x[0] for x in cr.fetchall()]
287 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
288 child_ids = self.search(cr, uid, [('parent_id','=', proj.id)])
290 self.setActive(cr, uid, child_ids, value, context=None)
296 _name = "project.task"
297 _description = "Task"
299 _date_name = "date_start"
301 def _str_get(self, task, level=0, border='***', context=None):
302 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'+ \
303 border[0]+' '+(task.name or '')+'\n'+ \
304 (task.description or '')+'\n\n'
306 # Compute: effective_hours, total_hours, progress
307 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
309 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
310 hours = dict(cr.fetchall())
311 for task in self.browse(cr, uid, ids, context=context):
312 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
313 res[task.id]['progress'] = 0.0
314 if (task.remaining_hours + hours.get(task.id, 0.0)):
315 if task.state != 'done':
316 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99), 2)
318 if task.state in ('done', 'cancel'):
319 res[task.id]['progress'] = 100.0
320 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
323 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
324 return {'value':{'remaining_hours': planned - effective}}
326 def _default_project(self, cr, uid, context=None):
329 if 'project_id' in context and context['project_id']:
330 return int(context['project_id'])
333 #_sql_constraints = [
334 # ('remaining_hours', 'CHECK (remaining_hours>=0)', 'Please increase and review remaining hours ! It can not be smaller than 0.'),
337 def copy_data(self, cr, uid, id, default={}, context=None):
338 default = default or {}
339 default['work_ids'] = []
340 return super(task, self).copy_data(cr, uid, id, default, context)
342 def _check_dates(self, cr, uid, ids, context=None):
343 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
344 if task['date_start'] and task['date_end']:
345 if task['date_start'] > task['date_end']:
350 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the task without removing it. This is basically used for the management of templates of projects and tasks."),
351 'name': fields.char('Task Summary', size=128, required=True),
352 'description': fields.text('Description'),
353 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
354 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
355 'type': fields.many2one('project.task.type', 'Stage',),
356 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
357 help='If the task is created the state is \'Draft\'.\n If the task is started, the state becomes \'In Progress\'.\n If review is needed the task is in \'Pending\' state.\
358 \n If the task is over, the states is set to \'Done\'.'),
359 'create_date': fields.datetime('Create Date', readonly=True),
360 'date_start': fields.datetime('Starting Date'),
361 'date_end': fields.datetime('Ending Date'),
362 'date_deadline': fields.date('Deadline'),
363 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
364 help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
365 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
366 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
367 'notes': fields.text('Notes'),
368 '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.'),
369 '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."),
370 'remaining_hours': fields.float('Remaining Hours', digits=(16,4), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
371 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
372 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', store=True, help="Computed as: Time Spent / Total Time."),
373 'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', store=True, help="Computed as difference of the time estimated by the project manager and the real time to close the task."),
375 'user_id': fields.many2one('res.users', 'Assigned to'),
376 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
377 'partner_id': fields.many2one('res.partner', 'Partner'),
378 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
379 'manager_id': fields.related('project_id', 'category_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
380 'company_id': fields.many2one('res.company', 'Company'),
389 'project_id': _default_project,
390 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
393 _order = "sequence, priority, date_start, id"
396 (_check_dates, 'Error! task start-date must be lower then task end-date.', ['date_start', 'date_end'])
399 # Override view according to the company definition
402 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
403 users_obj = self.pool.get('res.users')
404 obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
405 tm = obj_tm and obj_tm.name or 'Hours'
407 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
409 if tm in ['Hours','Hour']:
412 eview = etree.fromstring(res['arch'])
414 def _check_rec(eview):
415 if eview.attrib.get('widget','') == 'float_time':
416 eview.set('widget','float')
423 res['arch'] = etree.tostring(eview)
425 for f in res['fields']:
426 if 'Hours' in res['fields'][f]['string']:
427 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
431 def do_close(self, cr, uid, ids, *args):
433 mod_obj = self.pool.get('ir.model.data')
434 request = self.pool.get('res.request')
435 tasks = self.browse(cr, uid, ids)
441 project = task.project_id
443 if project.warn_manager and project.user_id and (project.user_id.id != uid):
444 request.create(cr, uid, {
445 'name': _("Task '%s' closed") % task.name,
448 'act_to': project.user_id.id,
449 'ref_partner_id': task.partner_id.id,
450 'ref_doc1': 'project.task,%d'% (task.id,),
451 'ref_doc2': 'project.project,%d'% (project.id,),
453 elif project.warn_manager and cntx.get('mail_send',True):
455 message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
456 self.log(cr, uid, task.id, message)
458 for parent_id in task.parent_ids:
459 if parent_id.state in ('pending','draft'):
461 for child in parent_id.child_ids:
462 if child.id != task.id and child.state not in ('done','cancelled'):
465 self.do_reopen(cr, uid, [parent_id.id])
467 model_data_ids = mod_obj.search(cr,uid,[('model','=','ir.ui.view'),('name','=','view_project_close_task')])
468 resource_id = mod_obj.read(cr, uid, model_data_ids, fields=['res_id'])[0]['res_id']
469 cntx.update({'task_id': task_id})
471 'name': _('Email Send to Customer'),
473 'context': cntx, # improve me
474 'view_mode': 'tree,form',
475 'res_model': 'close.task',
476 'views': [(resource_id,'form')],
477 'type': 'ir.actions.act_window',
482 self.write(cr, uid, [task_id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
485 def do_reopen(self, cr, uid, ids, *args):
486 request = self.pool.get('res.request')
487 tasks = self.browse(cr, uid, ids)
489 project = task.project_id
490 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
491 request.create(cr, uid, {
492 'name': _("Task '%s' set in progress") % task.name,
495 'act_to': project.user_id.id,
496 'ref_partner_id': task.partner_id.id,
497 'ref_doc1': 'project.task,%d' % task.id,
498 'ref_doc2': 'project.project,%d' % project.id,
501 self.write(cr, uid, [task.id], {'state': 'open'})
504 def do_cancel(self, cr, uid, ids, *args):
505 request = self.pool.get('res.request')
506 tasks = self.browse(cr, uid, ids)
508 project = task.project_id
509 if project.warn_manager and project.user_id and (project.user_id.id != uid):
510 request.create(cr, uid, {
511 'name': _("Task '%s' cancelled") % task.name,
514 'act_to': project.user_id.id,
515 'ref_partner_id': task.partner_id.id,
516 'ref_doc1': 'project.task,%d' % task.id,
517 'ref_doc2': 'project.project,%d' % project.id,
519 message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
520 self.log(cr, uid, task.id, message)
521 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
524 def do_open(self, cr, uid, ids, *args):
525 tasks= self.browse(cr,uid,ids)
527 self.write(cr, uid, [t.id], {'state': 'open', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S'),})
528 message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
529 self.log(cr, uid, t.id, message)
532 def do_draft(self, cr, uid, ids, *args):
533 self.write(cr, uid, ids, {'state': 'draft'})
537 def do_pending(self, cr, uid, ids, *args):
538 self.write(cr, uid, ids, {'state': 'pending'})
539 for (id, name) in self.name_get(cr, uid, ids):
540 message = _('Task ') + " '" + name + "' "+ _("is Pending.")
541 self.log(cr, uid, id, message)
544 def next_type(self, cr, uid, ids, *args):
545 for typ in self.browse(cr, uid, ids):
547 types = map(lambda x:x.id, typ.project_id.type_ids or [])
550 self.write(cr, uid, typ.id, {'type': types[0]})
551 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
552 index = types.index(typeid)
553 self.write(cr, uid, typ.id, {'type': types[index+1]})
556 def prev_type(self, cr, uid, ids, *args):
557 for typ in self.browse(cr, uid, ids):
559 types = map(lambda x:x.id, typ.project_id.type_ids)
561 if typeid and typeid in types:
562 index = types.index(typeid)
563 self.write(cr, uid, typ.id, {'type': index and types[index-1] or False})
568 class project_work(osv.osv):
569 _name = "project.task.work"
570 _description = "Task Work"
572 'name': fields.char('Work summary', size=128),
573 'date': fields.datetime('Date'),
574 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
575 'hours': fields.float('Time Spent'),
576 'user_id': fields.many2one('res.users', 'Done by', required=True),
577 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
581 'user_id': lambda obj, cr, uid, context: uid,
582 'date': time.strftime('%Y-%m-%d %H:%M:%S')
587 def create(self, cr, uid, vals, *args, **kwargs):
588 if 'hours' in vals and (not vals['hours']):
590 if 'task_id' in vals:
591 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
592 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
594 def write(self, cr, uid, ids, vals, context=None):
597 if 'hours' in vals and (not vals['hours']):
600 for work in self.browse(cr, uid, ids, context):
601 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))
602 return super(project_work,self).write(cr, uid, ids, vals, context)
604 def unlink(self, cr, uid, ids, *args, **kwargs):
605 for work in self.browse(cr, uid, ids):
606 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
607 return super(project_work,self).unlink(cr, uid, ids, *args, **kwargs)
611 class config_compute_remaining(osv.osv_memory):
612 _name='config.compute.remaining'
614 def _get_remaining(self,cr, uid, ctx):
615 task_obj = self.pool.get('project.task')
616 if 'active_id' in ctx:
617 return task_obj.browse(cr, uid, ctx['active_id'], context=ctx).remaining_hours
621 'remaining_hours' : fields.float('Remaining Hours', digits=(16,2), help="Put here the remaining hours required to close the task."),
624 def _get_analytic_account(self, cr, uid, context={}):
625 if context.get('account_id', False):
626 return context.get('account_id')
630 'account_id' : _get_analytic_account,
634 'remaining_hours': _get_remaining
637 def compute_hours(self, cr, uid, ids, context=None):
640 task_obj = self.pool.get('project.task')
641 request = self.pool.get('res.request')
642 if 'active_id' in context:
643 remaining_hrs=self.browse(cr,uid,ids)[0].remaining_hours
644 task_obj.write(cr,uid,context['active_id'],{'remaining_hours':remaining_hrs})
645 if context.get('button_reactivate', False):
646 tasks = task_obj.browse(cr, uid, [context['active_id']], context=context)
648 project = task.project_id
649 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
650 request.create(cr, uid, {
651 'name': _("Task '%s' set in progress") % task.name,
654 'act_to': project.user_id.id,
655 'ref_partner_id': task.partner_id.id,
656 'ref_doc1': 'project.task,%d' % task.id,
657 'ref_doc2': 'project.project,%d' % project.id,
659 task_obj.write(cr, uid, [task.id], {'state': 'open'})
661 'type': 'ir.actions.act_window_close',
664 config_compute_remaining()
666 class message(osv.osv):
667 _name = "project.message"
668 _description = "Message"
671 'subject': fields.char('Subject', size=128, required="True"),
672 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
673 'date': fields.date('Date', required=1),
674 'user_id': fields.many2one('res.users', 'User', required="True"),
675 'description': fields.text('Description'),
678 def _default_project(self, cr, uid, context=None):
681 if 'project_id' in context and context['project_id']:
682 return int(context['project_id'])
686 'user_id': lambda self,cr,uid,ctx: uid,
687 'date': time.strftime('%Y-%m-%d'),
688 'project_id': _default_project
693 class users(osv.osv):
694 _inherit = 'res.users'
695 _description = "Users"
697 'context_project_id': fields.many2one('project.project', 'Project')
702 class account_analytic_account(osv.osv):
704 _inherit = 'account.analytic.account'
705 _description = 'Analytic Account'
707 def create(self, cr, uid, vals, context=None):
710 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
711 vals['child_ids'] = []
712 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
714 account_analytic_account()
716 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: