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 datetime, date
25 from operator import itemgetter
26 from itertools import groupby
28 from tools.misc import flatten
29 from tools.translate import _
30 from osv import fields, osv
33 class project_task_type(osv.osv):
34 _name = 'project.task.type'
35 _description = 'Task Stage'
38 'name': fields.char('Stage Name', required=True, size=64, translate=True),
39 'description': fields.text('Description'),
40 'sequence': fields.integer('Sequence'),
49 class project(osv.osv):
50 _name = "project.project"
51 _description = "Project"
52 _inherits = {'account.analytic.account': "analytic_account_id"}
54 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
56 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
57 if context and context.has_key('user_prefence') and context['user_prefence']:
58 cr.execute("""SELECT project.id FROM project_project project
59 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
60 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
61 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
62 return [(r[0]) for r in cr.fetchall()]
63 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
64 context=context, count=count)
66 def _complete_name(self, cr, uid, ids, name, args, context=None):
68 for m in self.browse(cr, uid, ids, context=context):
69 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
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)
87 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
94 project_id''', (tuple(ids),))
95 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
96 for project in self.browse(cr, uid, ids, context=context):
97 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
99 'planned_hours': s[0],
100 'effective_hours': s[2],
102 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
106 def _get_project_task(self, cr, uid, ids, context=None):
110 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
111 if task.project_id: result[task.project_id.id] = True
114 def _get_project_work(self, cr, uid, ids, context=None):
118 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
119 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
122 def unlink(self, cr, uid, ids, *args, **kwargs):
123 for proj in self.browse(cr, uid, ids):
125 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
126 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
129 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
130 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
131 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
132 'analytic_account_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.", ondelete="cascade", required=True),
133 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
134 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
135 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
136 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
137 '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 and its child projects.",
139 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
140 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
142 '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 and its child projects.",
144 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
145 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
147 '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 and its child projects.",
149 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
150 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
152 'progress_rate': fields.function(_progress_rate, multi="progress", method=True, string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo.",
154 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
155 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
156 'project.task.work': (_get_project_work, ['hours'], 10),
158 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
159 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
160 '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.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
161 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
170 # TODO: Why not using a SQL contraints ?
171 def _check_dates(self, cr, uid, ids):
172 for leave in self.read(cr, uid, ids, ['date_start', 'date']):
173 if leave['date_start'] and leave['date']:
174 if leave['date_start'] > leave['date']:
179 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
182 def set_template(self, cr, uid, ids, context=None):
183 res = self.setActive(cr, uid, ids, value=False, context=context)
186 def set_done(self, cr, uid, ids, context=None):
187 task_obj = self.pool.get('project.task')
188 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
189 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
190 self.write(cr, uid, ids, {'state':'close'}, context=context)
191 for (id, name) in self.name_get(cr, uid, ids):
192 message = _("The project '%s' has been closed.") % name
193 self.log(cr, uid, id, message)
196 def set_cancel(self, cr, uid, ids, context=None):
197 task_obj = self.pool.get('project.task')
198 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
199 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
200 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
203 def set_pending(self, cr, uid, ids, context=None):
204 self.write(cr, uid, ids, {'state':'pending'}, context=context)
207 def set_open(self, cr, uid, ids, context=None):
208 self.write(cr, uid, ids, {'state':'open'}, context=context)
211 def reset_project(self, cr, uid, ids, context=None):
212 res = self.setActive(cr, uid, ids, value=True, context=context)
213 for (id, name) in self.name_get(cr, uid, ids):
214 message = _("The project '%s' has been opened.") % name
215 self.log(cr, uid, id, message)
218 def copy(self, cr, uid, id, default={}, context=None):
222 proj = self.browse(cr, uid, id, context=context)
223 default = default or {}
224 context['active_test'] = False
225 default['state'] = 'open'
226 if not default.get('name', False):
227 default['name'] = proj.name + _(' (copy)')
228 res = super(project, self).copy(cr, uid, id, default, context)
232 def duplicate_template(self, cr, uid, ids, context=None):
235 project_obj = self.pool.get('project.project')
236 data_obj = self.pool.get('ir.model.data')
238 for proj in self.browse(cr, uid, ids, context=context):
239 parent_id = context.get('parent_id', False)
240 context.update({'analytic_project_copy': True})
241 new_date_start = time.strftime('%Y-%m-%d')
243 if proj.date_start and proj.date:
244 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
245 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
246 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
247 new_id = project_obj.copy(cr, uid, proj.id, default = {
248 'name': proj.name +_(' (copy)'),
250 'date_start':new_date_start,
252 'parent_id':parent_id}, context=context)
253 result.append(new_id)
255 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
256 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
258 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
260 if result and len(result):
262 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
263 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
264 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
265 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
266 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
267 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
269 'name': _('Projects'),
271 'view_mode': 'form,tree',
272 'res_model': 'project.project',
275 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
276 'type': 'ir.actions.act_window',
277 'search_view_id': search_view['res_id'],
281 # set active value for a project, its sub projects and its tasks
282 def setActive(self, cr, uid, ids, value=True, context=None):
283 task_obj = self.pool.get('project.task')
284 for proj in self.browse(cr, uid, ids, context=None):
285 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
286 cr.execute('select id from project_task where project_id=%s', (proj.id,))
287 tasks_id = [x[0] for x in cr.fetchall()]
289 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
290 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
292 self.setActive(cr, uid, child_ids, value, context=None)
297 class users(osv.osv):
298 _inherit = 'res.users'
300 'context_project_id': fields.many2one('project.project', 'Project')
305 _name = "project.task"
306 _description = "Task"
308 _date_name = "date_start"
310 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
311 obj_project = self.pool.get('project.project')
313 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
314 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
315 if id and isinstance(id, (long, int)):
316 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
317 args.append(('active', '=', False))
318 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
320 def _str_get(self, task, level=0, border='***', context=None):
321 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'+ \
322 border[0]+' '+(task.name or '')+'\n'+ \
323 (task.description or '')+'\n\n'
325 # Compute: effective_hours, total_hours, progress
326 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
327 project_obj = self.pool.get('project.project')
329 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
330 hours = dict(cr.fetchall())
331 for task in self.browse(cr, uid, ids, context=context):
332 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': (task.remaining_hours or 0.0) + hours.get(task.id, 0.0)}
333 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
334 res[task.id]['progress'] = 0.0
335 if (task.remaining_hours + hours.get(task.id, 0.0)):
336 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
337 if task.state in ('done','cancelled'):
338 res[task.id]['progress'] = 100.0
342 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
343 if remaining and not planned:
344 return {'value':{'planned_hours': remaining}}
347 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
348 return {'value':{'remaining_hours': planned - effective}}
350 def _default_project(self, cr, uid, context=None):
353 if 'project_id' in context and context['project_id']:
354 return int(context['project_id'])
357 def copy_data(self, cr, uid, id, default={}, context=None):
358 default = default or {}
359 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
360 if not default.get('remaining_hours', False):
361 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
362 default['active'] = True
363 default['type_id'] = False
364 if not default.get('name', False):
365 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
366 return super(task, self).copy_data(cr, uid, id, default, context)
368 def _check_dates(self, cr, uid, ids, context=None):
369 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
370 if task['date_start'] and task['date_end']:
371 if task['date_start'] > task['date_end']:
375 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
377 for task in self.browse(cr, uid, ids, context=context):
380 if task.project_id.active == False or task.project_id.state == 'template':
384 def _get_task(self, cr, uid, ids, context=None):
388 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
389 if work.task_id: result[work.task_id.id] = True
393 'active': fields.function(_is_template, method=True, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
394 'name': fields.char('Task Summary', size=128, required=True),
395 'description': fields.text('Description'),
396 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
397 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
398 'type_id': fields.many2one('project.task.type', 'Stage'),
399 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
400 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.\
401 \n If the task is over, the states is set to \'Done\'.'),
402 'create_date': fields.datetime('Create Date', readonly=True),
403 'date_start': fields.datetime('Starting Date'),
404 'date_end': fields.datetime('Ending Date'),
405 'date_deadline': fields.date('Deadline'),
406 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
407 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
408 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
409 'notes': fields.text('Notes'),
410 'planned_hours': fields.float('Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
411 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
413 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
414 'project.task.work': (_get_task, ['hours'], 10),
416 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
417 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
419 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
420 'project.task.work': (_get_task, ['hours'], 10),
422 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
424 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
425 'project.task.work': (_get_task, ['hours'], 10),
427 'delay_hours': fields.function(_hours_get, method=True, string='Delay Hours', multi='hours', help="Computed as difference of the time estimated by the project manager and the real time to close the task.",
429 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
430 'project.task.work': (_get_task, ['hours'], 10),
432 'user_id': fields.many2one('res.users', 'Assigned to'),
433 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
434 'partner_id': fields.many2one('res.partner', 'Partner'),
435 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
436 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
437 'company_id': fields.many2one('res.company', 'Company'),
438 'id': fields.integer('ID'),
447 'project_id': _default_project,
448 'user_id': lambda obj, cr, uid, context: uid,
449 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
452 _order = "sequence, priority, date_start, id"
454 def _check_recursion(self, cr, uid, ids):
455 obj_task = self.browse(cr, uid, ids[0])
456 parent_ids = [x.id for x in obj_task.parent_ids]
457 children_ids = [x.id for x in obj_task.child_ids]
459 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
463 cr.execute('SELECT DISTINCT task_id '\
464 'FROM project_task_parent_rel '\
465 'WHERE parent_id IN %s', (tuple(ids),))
466 child_ids = map(lambda x: x[0], cr.fetchall())
468 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
471 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
472 if (list(set(parent_ids).intersection(set(s_ids)))):
479 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
482 # Override view according to the company definition
485 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
486 users_obj = self.pool.get('res.users')
488 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
489 # this should be safe (no context passed to avoid side-effects)
490 obj_tm = users_obj.browse(cr, 1, uid).company_id.project_time_mode_id
491 tm = obj_tm and obj_tm.name or 'Hours'
493 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
495 if tm in ['Hours','Hour']:
498 eview = etree.fromstring(res['arch'])
500 def _check_rec(eview):
501 if eview.attrib.get('widget','') == 'float_time':
502 eview.set('widget','float')
509 res['arch'] = etree.tostring(eview)
511 for f in res['fields']:
512 if 'Hours' in res['fields'][f]['string']:
513 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
516 def action_close(self, cr, uid, ids, context=None):
517 # This action open wizard to send email to partner or project manager after close task.
518 project_id = len(ids) and ids[0] or False
519 if not project_id: return False
520 task = self.browse(cr, uid, project_id, context=context)
521 project = task.project_id
522 res = self.do_close(cr, uid, [project_id], context=context)
523 if project.warn_manager or project.warn_customer:
525 'name': _('Send Email after close task'),
528 'res_model': 'project.task.close',
529 'type': 'ir.actions.act_window',
532 'context': {'active_id': task.id}
536 def do_close(self, cr, uid, ids, context=None):
542 request = self.pool.get('res.request')
543 for task in self.browse(cr, uid, ids, context=context):
544 project = task.project_id
546 # Send request to project manager
547 if project.warn_manager and project.user_id and (project.user_id.id != uid):
548 request.create(cr, uid, {
549 'name': _("Task '%s' closed") % task.name,
552 'act_to': project.user_id.id,
553 'ref_partner_id': task.partner_id.id,
554 'ref_doc1': 'project.task,%d'% (task.id,),
555 'ref_doc2': 'project.project,%d'% (project.id,),
558 for parent_id in task.parent_ids:
559 if parent_id.state in ('pending','draft'):
561 for child in parent_id.child_ids:
562 if child.id != task.id and child.state not in ('done','cancelled'):
565 self.do_reopen(cr, uid, [parent_id.id])
566 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
567 message = _("The task '%s' is done") % (task.name,)
568 self.log(cr, uid, task.id, message)
571 def do_reopen(self, cr, uid, ids, context=None):
574 request = self.pool.get('res.request')
576 for task in self.browse(cr, uid, ids, context=context):
577 project = task.project_id
578 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
579 request.create(cr, uid, {
580 'name': _("Task '%s' set in progress") % task.name,
583 'act_to': project.user_id.id,
584 'ref_partner_id': task.partner_id.id,
585 'ref_doc1': 'project.task,%d' % task.id,
586 'ref_doc2': 'project.project,%d' % project.id,
589 self.write(cr, uid, [task.id], {'state': 'open'})
593 def do_cancel(self, cr, uid, ids, *args):
594 request = self.pool.get('res.request')
595 tasks = self.browse(cr, uid, ids)
597 project = task.project_id
598 if project.warn_manager and project.user_id and (project.user_id.id != uid):
599 request.create(cr, uid, {
600 'name': _("Task '%s' cancelled") % task.name,
603 'act_to': project.user_id.id,
604 'ref_partner_id': task.partner_id.id,
605 'ref_doc1': 'project.task,%d' % task.id,
606 'ref_doc2': 'project.project,%d' % project.id,
608 message = _("The task '%s' is cancelled.") % (task.name,)
609 self.log(cr, uid, task.id, message)
610 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
613 def do_open(self, cr, uid, ids, *args):
614 tasks= self.browse(cr,uid,ids)
616 data = {'state': 'open'}
618 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
619 self.write(cr, uid, [t.id], data)
620 message = _("The task '%s' is opened.") % (t.name,)
621 self.log(cr, uid, t.id, message)
624 def do_draft(self, cr, uid, ids, *args):
625 self.write(cr, uid, ids, {'state': 'draft'})
628 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
630 Delegate Task to another users.
634 task = self.browse(cr, uid, task_id, context=context)
635 new_task_id = self.copy(cr, uid, task.id, {
636 'name': delegate_data['name'],
637 'user_id': delegate_data['user_id'],
638 'planned_hours': delegate_data['planned_hours'],
639 'remaining_hours': delegate_data['planned_hours'],
640 'parent_ids': [(6, 0, [task.id])],
642 'description': delegate_data['new_task_description'] or '',
646 newname = delegate_data['prefix'] or ''
647 self.write(cr, uid, [task.id], {
648 'remaining_hours': delegate_data['planned_hours_me'],
649 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
652 if delegate_data['state'] == 'pending':
653 self.do_pending(cr, uid, [task.id], context)
655 self.do_close(cr, uid, [task.id], context)
656 user_pool = self.pool.get('res.users')
657 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
658 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
659 self.log(cr, uid, task.id, message)
662 def do_pending(self, cr, uid, ids, *args):
663 self.write(cr, uid, ids, {'state': 'pending'})
664 for (id, name) in self.name_get(cr, uid, ids):
665 message = _("The task '%s' is pending.") % name
666 self.log(cr, uid, id, message)
669 def next_type(self, cr, uid, ids, *args):
670 for task in self.browse(cr, uid, ids):
671 typeid = task.type_id.id
672 types = map(lambda x:x.id, task.project_id.type_ids or [])
675 self.write(cr, uid, task.id, {'type_id': types[0]})
676 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
677 index = types.index(typeid)
678 self.write(cr, uid, task.id, {'type_id': types[index+1]})
681 def prev_type(self, cr, uid, ids, *args):
682 for task in self.browse(cr, uid, ids):
683 typeid = task.type_id.id
684 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
686 if typeid and typeid in types:
687 index = types.index(typeid)
688 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
693 class project_work(osv.osv):
694 _name = "project.task.work"
695 _description = "Project Task Work"
697 'name': fields.char('Work summary', size=128),
698 'date': fields.datetime('Date'),
699 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
700 'hours': fields.float('Time Spent'),
701 'user_id': fields.many2one('res.users', 'Done by', required=True),
702 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
706 'user_id': lambda obj, cr, uid, context: uid,
707 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
711 def create(self, cr, uid, vals, *args, **kwargs):
712 if 'hours' in vals and (not vals['hours']):
714 if 'task_id' in vals:
715 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
716 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
718 def write(self, cr, uid, ids,vals,context={}):
719 if 'hours' in vals and (not vals['hours']):
722 for work in self.browse(cr, uid, ids, context):
723 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))
724 return super(project_work,self).write(cr, uid, ids, vals, context)
726 def unlink(self, cr, uid, ids, *args, **kwargs):
727 for work in self.browse(cr, uid, ids):
728 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
729 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
732 class account_analytic_account(osv.osv):
734 _inherit = 'account.analytic.account'
735 _description = 'Analytic Account'
737 def create(self, cr, uid, vals, context=None):
740 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
741 vals['child_ids'] = []
742 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
744 account_analytic_account()
746 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: