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),
157 '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)]}),
158 '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)]}),
159 '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)]}),
160 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
169 # TODO: Why not using a SQL contraints ?
170 def _check_dates(self, cr, uid, ids):
171 for leave in self.read(cr, uid, ids, ['date_start', 'date']):
172 if leave['date_start'] and leave['date']:
173 if leave['date_start'] > leave['date']:
178 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
181 def set_template(self, cr, uid, ids, context=None):
182 res = self.setActive(cr, uid, ids, value=False, context=context)
185 def set_done(self, cr, uid, ids, context=None):
186 task_obj = self.pool.get('project.task')
187 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
188 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
189 self.write(cr, uid, ids, {'state':'close'}, context=context)
190 for (id, name) in self.name_get(cr, uid, ids):
191 message = _("The project '%s' has been closed.") % name
192 self.log(cr, uid, id, message)
195 def set_cancel(self, cr, uid, ids, context=None):
196 task_obj = self.pool.get('project.task')
197 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
198 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
199 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
202 def set_pending(self, cr, uid, ids, context=None):
203 self.write(cr, uid, ids, {'state':'pending'}, context=context)
206 def set_open(self, cr, uid, ids, context=None):
207 self.write(cr, uid, ids, {'state':'open'}, context=context)
210 def reset_project(self, cr, uid, ids, context=None):
211 res = self.setActive(cr, uid, ids, value=True, context=context)
212 for (id, name) in self.name_get(cr, uid, ids):
213 message = _("The project '%s' has been opened.") % name
214 self.log(cr, uid, id, message)
217 def copy(self, cr, uid, id, default={}, context=None):
221 proj = self.browse(cr, uid, id, context=context)
222 default = default or {}
223 context['active_test'] = False
224 default['state'] = 'open'
225 if not default.get('name', False):
226 default['name'] = proj.name + _(' (copy)')
227 res = super(project, self).copy(cr, uid, id, default, context)
231 def duplicate_template(self, cr, uid, ids, context=None):
234 project_obj = self.pool.get('project.project')
235 data_obj = self.pool.get('ir.model.data')
237 for proj in self.browse(cr, uid, ids, context=context):
238 parent_id = context.get('parent_id', False)
239 context.update({'analytic_project_copy': True})
240 new_date_start = time.strftime('%Y-%m-%d')
242 if proj.date_start and proj.date:
243 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
244 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
245 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
246 new_id = project_obj.copy(cr, uid, proj.id, default = {
247 'name': proj.name +_(' (copy)'),
249 'date_start':new_date_start,
251 'parent_id':parent_id}, context=context)
252 result.append(new_id)
254 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
255 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
257 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
259 if result and len(result):
261 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
262 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
263 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
264 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
265 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
266 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
268 'name': _('Projects'),
270 'view_mode': 'form,tree',
271 'res_model': 'project.project',
274 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
275 'type': 'ir.actions.act_window',
276 'search_view_id': search_view['res_id'],
280 # set active value for a project, its sub projects and its tasks
281 def setActive(self, cr, uid, ids, value=True, context=None):
282 task_obj = self.pool.get('project.task')
283 for proj in self.browse(cr, uid, ids, context=None):
284 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
285 cr.execute('select id from project_task where project_id=%s', (proj.id,))
286 tasks_id = [x[0] for x in cr.fetchall()]
288 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
289 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
291 self.setActive(cr, uid, child_ids, value, context=None)
296 class users(osv.osv):
297 _inherit = 'res.users'
299 'context_project_id': fields.many2one('project.project', 'Project')
304 _name = "project.task"
305 _description = "Task"
307 _date_name = "date_start"
309 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
310 obj_project = self.pool.get('project.project')
312 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
313 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
314 if id and isinstance(id, (long, int)):
315 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
316 args.append(('active', '=', False))
317 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
319 def _str_get(self, task, level=0, border='***', context=None):
320 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'+ \
321 border[0]+' '+(task.name or '')+'\n'+ \
322 (task.description or '')+'\n\n'
324 # Compute: effective_hours, total_hours, progress
325 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
326 project_obj = self.pool.get('project.project')
328 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
329 hours = dict(cr.fetchall())
330 for task in self.browse(cr, uid, ids, context=context):
331 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)}
332 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
333 res[task.id]['progress'] = 0.0
334 if (task.remaining_hours + hours.get(task.id, 0.0)):
335 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
336 if task.state in ('done','cancelled'):
337 res[task.id]['progress'] = 100.0
341 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
342 if remaining and not planned:
343 return {'value':{'planned_hours': remaining}}
346 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
347 return {'value':{'remaining_hours': planned - effective}}
349 def _default_project(self, cr, uid, context=None):
352 if 'project_id' in context and context['project_id']:
353 return int(context['project_id'])
356 def copy_data(self, cr, uid, id, default={}, context=None):
357 default = default or {}
358 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
359 if not default.get('remaining_hours', False):
360 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
361 default['active'] = True
362 default['type_id'] = False
363 if not default.get('name', False):
364 default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)')
365 return super(task, self).copy_data(cr, uid, id, default, context)
367 def _check_dates(self, cr, uid, ids, context=None):
368 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
369 if task['date_start'] and task['date_end']:
370 if task['date_start'] > task['date_end']:
374 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
376 for task in self.browse(cr, uid, ids, context=context):
379 if task.project_id.active == False or task.project_id.state == 'template':
383 def _get_task(self, cr, uid, ids, context=None):
387 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
388 if work.task_id: result[work.task_id.id] = True
392 '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."),
393 'name': fields.char('Task Summary', size=128, required=True),
394 'description': fields.text('Description'),
395 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
396 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
397 'type_id': fields.many2one('project.task.type', 'Stage'),
398 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
399 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.\
400 \n If the task is over, the states is set to \'Done\'.'),
401 'create_date': fields.datetime('Create Date', readonly=True),
402 'date_start': fields.datetime('Starting Date'),
403 'date_end': fields.datetime('Ending Date'),
404 'date_deadline': fields.date('Deadline'),
405 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
406 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
407 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
408 'notes': fields.text('Notes'),
409 '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.'),
410 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
412 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
413 'project.task.work': (_get_task, ['hours'], 10),
415 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
416 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
418 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
419 'project.task.work': (_get_task, ['hours'], 10),
421 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
423 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
424 'project.task.work': (_get_task, ['hours'], 10),
426 '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.",
428 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
429 'project.task.work': (_get_task, ['hours'], 10),
431 'user_id': fields.many2one('res.users', 'Assigned to'),
432 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
433 'partner_id': fields.many2one('res.partner', 'Partner'),
434 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
435 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
436 'company_id': fields.many2one('res.company', 'Company'),
437 'id': fields.integer('ID'),
446 'project_id': _default_project,
447 'user_id': lambda obj, cr, uid, context: uid,
448 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
451 _order = "sequence, priority, date_start, id"
453 def _check_recursion(self, cr, uid, ids):
454 obj_task = self.browse(cr, uid, ids[0])
455 parent_ids = [x.id for x in obj_task.parent_ids]
456 children_ids = [x.id for x in obj_task.child_ids]
458 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
462 cr.execute('SELECT DISTINCT task_id '\
463 'FROM project_task_parent_rel '\
464 'WHERE parent_id IN %s', (tuple(ids),))
465 child_ids = map(lambda x: x[0], cr.fetchall())
467 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
470 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
471 if (list(set(parent_ids).intersection(set(s_ids)))):
478 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
481 # Override view according to the company definition
484 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
485 users_obj = self.pool.get('res.users')
487 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
488 # this should be safe (no context passed to avoid side-effects)
489 obj_tm = users_obj.browse(cr, 1, uid).company_id.project_time_mode_id
490 tm = obj_tm and obj_tm.name or 'Hours'
492 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
494 if tm in ['Hours','Hour']:
497 eview = etree.fromstring(res['arch'])
499 def _check_rec(eview):
500 if eview.attrib.get('widget','') == 'float_time':
501 eview.set('widget','float')
508 res['arch'] = etree.tostring(eview)
510 for f in res['fields']:
511 if 'Hours' in res['fields'][f]['string']:
512 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
515 def action_close(self, cr, uid, ids, context=None):
516 # This action open wizard to send email to partner or project manager after close task.
517 project_id = len(ids) and ids[0] or False
518 if not project_id: return False
519 task = self.browse(cr, uid, project_id, context=context)
520 project = task.project_id
521 res = self.do_close(cr, uid, [project_id], context=context)
522 if project.warn_manager or project.warn_customer:
524 'name': _('Send Email after close task'),
527 'res_model': 'project.task.close',
528 'type': 'ir.actions.act_window',
531 'context': {'active_id': task.id}
535 def do_close(self, cr, uid, ids, context=None):
541 request = self.pool.get('res.request')
542 for task in self.browse(cr, uid, ids, context=context):
543 project = task.project_id
545 # Send request to project manager
546 if project.warn_manager and project.user_id and (project.user_id.id != uid):
547 request.create(cr, uid, {
548 'name': _("Task '%s' closed") % task.name,
551 'act_to': project.user_id.id,
552 'ref_partner_id': task.partner_id.id,
553 'ref_doc1': 'project.task,%d'% (task.id,),
554 'ref_doc2': 'project.project,%d'% (project.id,),
557 for parent_id in task.parent_ids:
558 if parent_id.state in ('pending','draft'):
560 for child in parent_id.child_ids:
561 if child.id != task.id and child.state not in ('done','cancelled'):
564 self.do_reopen(cr, uid, [parent_id.id])
565 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
566 message = _("The task '%s' is done") % (task.name,)
567 self.log(cr, uid, task.id, message)
570 def do_reopen(self, cr, uid, ids, context=None):
573 request = self.pool.get('res.request')
575 for task in self.browse(cr, uid, ids, context=context):
576 project = task.project_id
577 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
578 request.create(cr, uid, {
579 'name': _("Task '%s' set in progress") % task.name,
582 'act_to': project.user_id.id,
583 'ref_partner_id': task.partner_id.id,
584 'ref_doc1': 'project.task,%d' % task.id,
585 'ref_doc2': 'project.project,%d' % project.id,
588 self.write(cr, uid, [task.id], {'state': 'open'})
592 def do_cancel(self, cr, uid, ids, *args):
593 request = self.pool.get('res.request')
594 tasks = self.browse(cr, uid, ids)
596 project = task.project_id
597 if project.warn_manager and project.user_id and (project.user_id.id != uid):
598 request.create(cr, uid, {
599 'name': _("Task '%s' cancelled") % task.name,
602 'act_to': project.user_id.id,
603 'ref_partner_id': task.partner_id.id,
604 'ref_doc1': 'project.task,%d' % task.id,
605 'ref_doc2': 'project.project,%d' % project.id,
607 message = _("The task '%s' is cancelled.") % (task.name,)
608 self.log(cr, uid, task.id, message)
609 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
612 def do_open(self, cr, uid, ids, *args):
613 tasks= self.browse(cr,uid,ids)
615 data = {'state': 'open'}
617 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
618 self.write(cr, uid, [t.id], data)
619 message = _("The task '%s' is opened.") % (t.name,)
620 self.log(cr, uid, t.id, message)
623 def do_draft(self, cr, uid, ids, *args):
624 self.write(cr, uid, ids, {'state': 'draft'})
627 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
629 Delegate Task to another users.
633 task = self.browse(cr, uid, task_id, context=context)
634 new_task_id = self.copy(cr, uid, task.id, {
635 'name': delegate_data['name'],
636 'user_id': delegate_data['user_id'],
637 'planned_hours': delegate_data['planned_hours'],
638 'remaining_hours': delegate_data['planned_hours'],
639 'parent_ids': [(6, 0, [task.id])],
641 'description': delegate_data['new_task_description'] or '',
645 newname = delegate_data['prefix'] or ''
646 self.write(cr, uid, [task.id], {
647 'remaining_hours': delegate_data['planned_hours_me'],
648 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
651 if delegate_data['state'] == 'pending':
652 self.do_pending(cr, uid, [task.id], context)
654 self.do_close(cr, uid, [task.id], context)
655 user_pool = self.pool.get('res.users')
656 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
657 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
658 self.log(cr, uid, task.id, message)
661 def do_pending(self, cr, uid, ids, *args):
662 self.write(cr, uid, ids, {'state': 'pending'})
663 for (id, name) in self.name_get(cr, uid, ids):
664 message = _("The task '%s' is pending.") % name
665 self.log(cr, uid, id, message)
668 def next_type(self, cr, uid, ids, *args):
669 for task in self.browse(cr, uid, ids):
670 typeid = task.type_id.id
671 types = map(lambda x:x.id, task.project_id.type_ids or [])
674 self.write(cr, uid, task.id, {'type_id': types[0]})
675 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
676 index = types.index(typeid)
677 self.write(cr, uid, task.id, {'type_id': types[index+1]})
680 def prev_type(self, cr, uid, ids, *args):
681 for task in self.browse(cr, uid, ids):
682 typeid = task.type_id.id
683 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
685 if typeid and typeid in types:
686 index = types.index(typeid)
687 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
692 class project_work(osv.osv):
693 _name = "project.task.work"
694 _description = "Project Task Work"
696 'name': fields.char('Work summary', size=128),
697 'date': fields.datetime('Date'),
698 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
699 'hours': fields.float('Time Spent'),
700 'user_id': fields.many2one('res.users', 'Done by', required=True),
701 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
705 'user_id': lambda obj, cr, uid, context: uid,
706 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
710 def create(self, cr, uid, vals, *args, **kwargs):
711 if 'hours' in vals and (not vals['hours']):
713 if 'task_id' in vals:
714 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
715 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
717 def write(self, cr, uid, ids,vals,context={}):
718 if 'hours' in vals and (not vals['hours']):
721 for work in self.browse(cr, uid, ids, context):
722 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))
723 return super(project_work,self).write(cr, uid, ids, vals, context)
725 def unlink(self, cr, uid, ids, *args, **kwargs):
726 for work in self.browse(cr, uid, ids):
727 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
728 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
731 class account_analytic_account(osv.osv):
733 _inherit = 'account.analytic.account'
734 _description = 'Analytic Account'
736 def create(self, cr, uid, vals, context=None):
739 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
740 vals['child_ids'] = []
741 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
743 account_analytic_account()
745 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: