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):
108 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
109 if task.project_id: result[task.project_id.id] = True
112 def _get_project_work(self, cr, uid, ids, context=None):
114 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
115 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
118 def unlink(self, cr, uid, ids, *args, **kwargs):
119 for proj in self.browse(cr, uid, ids):
121 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
122 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
125 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
126 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
127 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
128 '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),
129 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
130 '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)]}),
131 '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)]}),
132 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
133 '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.",
135 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
136 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
138 '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.",
140 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
141 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
143 '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.",
145 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
146 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
148 '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.",
150 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
151 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
152 'project.task.work': (_get_project_work, ['hours'], 10),
154 '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)]}),
155 '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)]}),
156 '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)]}),
157 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
166 # TODO: Why not using a SQL contraints ?
167 def _check_dates(self, cr, uid, ids, context=None):
168 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
169 if leave['date_start'] and leave['date']:
170 if leave['date_start'] > leave['date']:
175 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
178 def set_template(self, cr, uid, ids, context=None):
179 res = self.setActive(cr, uid, ids, value=False, context=context)
182 def set_done(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', 'not in', ('cancelled', 'done'))])
185 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186 self.write(cr, uid, ids, {'state':'close'}, context=context)
187 for (id, name) in self.name_get(cr, uid, ids):
188 message = _("The project '%s' has been closed.") % name
189 self.log(cr, uid, id, message)
192 def set_cancel(self, cr, uid, ids, context=None):
193 task_obj = self.pool.get('project.task')
194 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
195 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
196 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
199 def set_pending(self, cr, uid, ids, context=None):
200 self.write(cr, uid, ids, {'state':'pending'}, context=context)
203 def set_open(self, cr, uid, ids, context=None):
204 self.write(cr, uid, ids, {'state':'open'}, context=context)
207 def reset_project(self, cr, uid, ids, context=None):
208 res = self.setActive(cr, uid, ids, value=True, context=context)
209 for (id, name) in self.name_get(cr, uid, ids):
210 message = _("The project '%s' has been opened.") % name
211 self.log(cr, uid, id, message)
214 def copy(self, cr, uid, id, default={}, context=None):
218 proj = self.browse(cr, uid, id, context=context)
219 default = default or {}
220 context['active_test'] = False
221 default['state'] = 'open'
222 if not default.get('name', False):
223 default['name'] = proj.name + _(' (copy)')
224 res = super(project, self).copy(cr, uid, id, default, context)
228 def duplicate_template(self, cr, uid, ids, context=None):
231 project_obj = self.pool.get('project.project')
232 data_obj = self.pool.get('ir.model.data')
234 for proj in self.browse(cr, uid, ids, context=context):
235 parent_id = context.get('parent_id', False)
236 context.update({'analytic_project_copy': True})
237 new_date_start = time.strftime('%Y-%m-%d')
239 if proj.date_start and proj.date:
240 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
241 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
242 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
243 new_id = project_obj.copy(cr, uid, proj.id, default = {
244 'name': proj.name +_(' (copy)'),
246 'date_start':new_date_start,
248 'parent_id':parent_id}, context=context)
249 result.append(new_id)
251 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
252 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
254 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
256 if result and len(result):
258 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
259 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
260 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
261 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
262 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
263 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
265 'name': _('Projects'),
267 'view_mode': 'form,tree',
268 'res_model': 'project.project',
271 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
272 'type': 'ir.actions.act_window',
273 'search_view_id': search_view['res_id'],
277 # set active value for a project, its sub projects and its tasks
278 def setActive(self, cr, uid, ids, value=True, context=None):
279 task_obj = self.pool.get('project.task')
280 for proj in self.browse(cr, uid, ids, context=None):
281 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
282 cr.execute('select id from project_task where project_id=%s', (proj.id,))
283 tasks_id = [x[0] for x in cr.fetchall()]
285 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
286 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
288 self.setActive(cr, uid, child_ids, value, context=None)
293 class users(osv.osv):
294 _inherit = 'res.users'
296 'context_project_id': fields.many2one('project.project', 'Project')
301 _name = "project.task"
302 _description = "Task"
304 _date_name = "date_start"
306 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
307 obj_project = self.pool.get('project.project')
309 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
310 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
311 if id and isinstance(id, (long, int)):
312 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
313 args.append(('active', '=', False))
314 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
316 def _str_get(self, task, level=0, border='***', context=None):
317 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'+ \
318 border[0]+' '+(task.name or '')+'\n'+ \
319 (task.description or '')+'\n\n'
321 # Compute: effective_hours, total_hours, progress
322 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
323 project_obj = self.pool.get('project.project')
325 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
326 hours = dict(cr.fetchall())
327 for task in self.browse(cr, uid, ids, context=context):
328 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)}
329 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
330 res[task.id]['progress'] = 0.0
331 if (task.remaining_hours + hours.get(task.id, 0.0)):
332 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
333 if task.state in ('done','cancelled'):
334 res[task.id]['progress'] = 100.0
338 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
339 if remaining and not planned:
340 return {'value':{'planned_hours': remaining}}
343 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
344 return {'value':{'remaining_hours': planned - effective}}
346 def onchange_project(self, cr, uid, id, project_id):
349 data = self.pool.get('project.project').browse(cr, uid, [project_id])
350 partner_id=data and data[0].parent_id.partner_id
352 return {'value':{'partner_id':partner_id.id}}
355 def _default_project(self, cr, uid, context=None):
358 if 'project_id' in context and context['project_id']:
359 return int(context['project_id'])
362 def copy_data(self, cr, uid, id, default={}, context=None):
363 default = default or {}
364 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
365 if not default.get('remaining_hours', False):
366 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
367 default['active'] = True
368 default['type_id'] = False
369 if not default.get('name', False):
370 default['name'] = self.browse(cr, uid, id, context=context).name
371 return super(task, self).copy_data(cr, uid, id, default, context)
373 def _check_dates(self, cr, uid, ids, context=None):
374 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
375 if task['date_start'] and task['date_end']:
376 if task['date_start'] > task['date_end']:
380 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
382 for task in self.browse(cr, uid, ids, context=context):
385 if task.project_id.active == False or task.project_id.state == 'template':
389 def _get_task(self, cr, uid, ids, context=None):
391 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
392 if work.task_id: result[work.task_id.id] = True
396 '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."),
397 'name': fields.char('Task Summary', size=128, required=True),
398 'description': fields.text('Description'),
399 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Priority'),
400 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
401 'type_id': fields.many2one('project.task.type', 'Stage'),
402 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
403 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.\
404 \n If the task is over, the states is set to \'Done\'.'),
405 'create_date': fields.datetime('Create Date', readonly=True),
406 'date_start': fields.datetime('Starting Date'),
407 'date_end': fields.datetime('Ending Date'),
408 'date_deadline': fields.date('Deadline'),
409 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
410 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
411 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
412 'notes': fields.text('Notes'),
413 '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.'),
414 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
416 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
417 'project.task.work': (_get_task, ['hours'], 10),
419 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
420 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
422 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
423 'project.task.work': (_get_task, ['hours'], 10),
425 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
427 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
428 'project.task.work': (_get_task, ['hours'], 10),
430 '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.",
432 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
433 'project.task.work': (_get_task, ['hours'], 10),
435 'user_id': fields.many2one('res.users', 'Assigned to'),
436 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
437 'partner_id': fields.many2one('res.partner', 'Partner'),
438 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
439 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
440 'company_id': fields.many2one('res.company', 'Company'),
441 'id': fields.integer('ID'),
450 'project_id': _default_project,
451 'user_id': lambda obj, cr, uid, context: uid,
452 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
455 _order = "sequence, priority, date_start, id"
457 def _check_recursion(self, cr, uid, ids, context=None):
458 obj_task = self.browse(cr, uid, ids[0], context=context)
459 parent_ids = [x.id for x in obj_task.parent_ids]
460 children_ids = [x.id for x in obj_task.child_ids]
462 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
466 cr.execute('SELECT DISTINCT task_id '\
467 'FROM project_task_parent_rel '\
468 'WHERE parent_id IN %s', (tuple(ids),))
469 child_ids = map(lambda x: x[0], cr.fetchall())
471 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
474 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
475 if (list(set(parent_ids).intersection(set(s_ids)))):
482 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids'])
485 # Override view according to the company definition
489 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
490 users_obj = self.pool.get('res.users')
492 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
493 # this should be safe (no context passed to avoid side-effects)
494 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
495 tm = obj_tm and obj_tm.name or 'Hours'
497 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
499 if tm in ['Hours','Hour']:
502 eview = etree.fromstring(res['arch'])
504 def _check_rec(eview):
505 if eview.attrib.get('widget','') == 'float_time':
506 eview.set('widget','float')
513 res['arch'] = etree.tostring(eview)
515 for f in res['fields']:
516 if 'Hours' in res['fields'][f]['string']:
517 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
520 def action_close(self, cr, uid, ids, context=None):
521 # This action open wizard to send email to partner or project manager after close task.
522 project_id = len(ids) and ids[0] or False
523 if not project_id: return False
524 task = self.browse(cr, uid, project_id, context=context)
525 project = task.project_id
526 res = self.do_close(cr, uid, [project_id], context=context)
527 if project.warn_manager or project.warn_customer:
529 'name': _('Send Email after close task'),
532 'res_model': 'project.task.close',
533 'type': 'ir.actions.act_window',
536 'context': {'active_id': task.id}
540 def do_close(self, cr, uid, ids, context=None):
544 request = self.pool.get('res.request')
545 for task in self.browse(cr, uid, ids, context=context):
546 project = task.project_id
548 # Send request to project manager
549 if project.warn_manager and project.user_id and (project.user_id.id != uid):
550 request.create(cr, uid, {
551 'name': _("Task '%s' closed") % task.name,
554 'act_to': project.user_id.id,
555 'ref_partner_id': task.partner_id.id,
556 'ref_doc1': 'project.task,%d'% (task.id,),
557 'ref_doc2': 'project.project,%d'% (project.id,),
560 for parent_id in task.parent_ids:
561 if parent_id.state in ('pending','draft'):
563 for child in parent_id.child_ids:
564 if child.id != task.id and child.state not in ('done','cancelled'):
567 self.do_reopen(cr, uid, [parent_id.id])
568 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
569 message = _("The task '%s' is done") % (task.name,)
570 self.log(cr, uid, task.id, message)
573 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.
632 task = self.browse(cr, uid, task_id, context=context)
633 new_task_id = self.copy(cr, uid, task.id, {
634 'name': delegate_data['name'],
635 'user_id': delegate_data['user_id'],
636 'planned_hours': delegate_data['planned_hours'],
637 'remaining_hours': delegate_data['planned_hours'],
638 'parent_ids': [(6, 0, [task.id])],
640 'description': delegate_data['new_task_description'] or '',
644 newname = delegate_data['prefix'] or ''
645 self.write(cr, uid, [task.id], {
646 'remaining_hours': delegate_data['planned_hours_me'],
647 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
650 if delegate_data['state'] == 'pending':
651 self.do_pending(cr, uid, [task.id], context)
653 self.do_close(cr, uid, [task.id], context=context)
654 user_pool = self.pool.get('res.users')
655 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
656 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
657 self.log(cr, uid, task.id, message)
660 def do_pending(self, cr, uid, ids, *args):
661 self.write(cr, uid, ids, {'state': 'pending'})
662 for (id, name) in self.name_get(cr, uid, ids):
663 message = _("The task '%s' is pending.") % name
664 self.log(cr, uid, id, message)
667 def next_type(self, cr, uid, ids, *args):
668 for task in self.browse(cr, uid, ids):
669 typeid = task.type_id.id
670 types = map(lambda x:x.id, task.project_id.type_ids or [])
673 self.write(cr, uid, task.id, {'type_id': types[0]})
674 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
675 index = types.index(typeid)
676 self.write(cr, uid, task.id, {'type_id': types[index+1]})
679 def prev_type(self, cr, uid, ids, *args):
680 for task in self.browse(cr, uid, ids):
681 typeid = task.type_id.id
682 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
684 if typeid and typeid in types:
685 index = types.index(typeid)
686 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
691 class project_work(osv.osv):
692 _name = "project.task.work"
693 _description = "Project Task Work"
695 'name': fields.char('Work summary', size=128),
696 'date': fields.datetime('Date'),
697 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
698 'hours': fields.float('Time Spent'),
699 'user_id': fields.many2one('res.users', 'Done by', required=True),
700 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
704 'user_id': lambda obj, cr, uid, context: uid,
705 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
709 def create(self, cr, uid, vals, *args, **kwargs):
710 if 'hours' in vals and (not vals['hours']):
712 if 'task_id' in vals:
713 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
714 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
716 def write(self, cr, uid, ids, vals, context=None):
717 if 'hours' in vals and (not vals['hours']):
720 for work in self.browse(cr, uid, ids, context=context):
721 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))
722 return super(project_work,self).write(cr, uid, ids, vals, context)
724 def unlink(self, cr, uid, ids, *args, **kwargs):
725 for work in self.browse(cr, uid, ids):
726 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
727 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
730 class account_analytic_account(osv.osv):
732 _inherit = 'account.analytic.account'
733 _description = 'Analytic Account'
735 def create(self, cr, uid, vals, context=None):
738 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
739 vals['child_ids'] = []
740 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
742 account_analytic_account()
744 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: