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
31 from tools import email_send as email
34 class project_task_type(osv.osv):
35 _name = 'project.task.type'
36 _description = 'Task Stage'
39 'name': fields.char('Stage Name', required=True, size=64, translate=True),
40 'description': fields.text('Description'),
41 'sequence': fields.integer('Sequence'),
50 class project(osv.osv):
51 _name = "project.project"
52 _description = "Project"
53 _inherits = {'account.analytic.account': "analytic_account_id"}
55 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
57 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
58 if context and context.has_key('user_prefence') and context['user_prefence']:
59 cr.execute("""SELECT project.id FROM project_project project
60 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
61 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
62 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
63 return [(r[0]) for r in cr.fetchall()]
64 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
65 context=context, count=count)
67 def _complete_name(self, cr, uid, ids, name, args, context=None):
69 for m in self.browse(cr, uid, ids, context=context):
70 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
73 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
74 partner_obj = self.pool.get('res.partner')
76 return {'value':{'contact_id': False, 'pricelist_id': False}}
77 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
78 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
79 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
80 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
82 def _get_user_and_default_uom_ids(self, cr, uid):
83 users_obj = self.pool.get('res.users')
84 model_data_obj = self.pool.get('ir.model.data')
85 model_data_id = model_data_obj._get_id(cr, uid, 'product', 'uom_hour')
86 default_uom = user_uom = model_data_obj.read(cr, uid, [model_data_id], ['res_id'])[0]['res_id']
87 obj_tm = users_obj.browse(cr, uid, uid).company_id.project_time_mode_id
90 return user_uom, default_uom
92 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
93 def _get_all_child_projects(ids):
94 """Recursively get child project ids"""
95 child_ids = flatten([project_hierarchy.get(idn, []) for idn in ids])
97 child_ids = _get_all_child_projects(child_ids)
98 return ids + child_ids
99 # END _get_all_child_projects
101 res = {}.fromkeys(ids, 0.0)
106 par_child_projects = {}
107 all_projects = list(ids)
109 # get project hierarchy:
110 cr.execute('''SELECT prp.id AS pr_parent_id, prpc.id AS pr_child_id
111 FROM account_analytic_account AS p
112 JOIN account_analytic_account AS c ON p.id = c.parent_id
113 JOIN project_project AS prp ON prp.analytic_account_id = p.id
114 JOIN project_project AS prpc ON prpc.analytic_account_id = c.id''')
116 project_hierarchy = dict((k, list(set([v[1] for v in itr]))) for k, itr in groupby(cr.fetchall(), itemgetter(0)))
119 child_projects = _get_all_child_projects([id])
120 par_child_projects[id] = child_projects
121 all_projects.extend(child_projects)
123 all_projects = dict.fromkeys(all_projects).keys()
125 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours)
132 project_id''',(tuple(all_projects),))
133 progress = dict(map(lambda x: (x[0], (x[1], x[2], x[3])), cr.fetchall()))
135 user_uom, def_uom = self._get_user_and_default_uom_ids(cr, uid)
136 for project in self.browse(cr, uid, par_child_projects.keys(), context=context):
138 tocompute = par_child_projects[project.id]
142 s[i] += progress.get(p, (0.0, 0.0, 0.0))[i]
144 uom_obj = self.pool.get('product.uom')
145 if user_uom != def_uom:
146 s[0] = uom_obj._compute_qty(cr, uid, user_uom, s[0], def_uom)
147 s[1] = uom_obj._compute_qty(cr, uid, user_uom, s[1], def_uom)
148 s[2] = uom_obj._compute_qty(cr, uid, user_uom, s[2], def_uom)
150 if project.state == 'close':
151 progress_rate = 100.0
153 progress_rate = s[1] and round(min(100.0 * s[2] / s[1], 99.99), 2)
156 'planned_hours': s[0],
157 'effective_hours': s[2],
159 'progress_rate': progress_rate
163 def unlink(self, cr, uid, ids, *args, **kwargs):
164 for proj in self.browse(cr, uid, ids):
166 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
167 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
170 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
171 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the project without removing it."),
172 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
173 '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),
174 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying a list of task"),
175 '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)]}),
176 '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)]}),
177 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
178 '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."),
179 '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."),
180 '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."),
181 '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."),
182 '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)]}),
183 '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)]}),
184 '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)]}),
185 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
194 'warn_manager': True,
197 def _check_dates(self, cr, uid, ids):
198 leave = self.read(cr, uid, ids[0], ['date_start', 'date'])
199 if leave['date_start'] and leave['date']:
200 if leave['date_start'] > leave['date']:
205 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
208 def set_template(self, cr, uid, ids, context=None):
209 res = self.setActive(cr, uid, ids, value=False, context=context)
212 def set_done(self, cr, uid, ids, context=None):
213 task_obj = self.pool.get('project.task')
214 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
215 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
216 self.write(cr, uid, ids, {'state':'close'}, context=context)
217 for (id, name) in self.name_get(cr, uid, ids):
218 message = _('Project ') + " '" + name + "' "+ _("is Closed.")
219 self.log(cr, uid, id, message)
222 def set_cancel(self, cr, uid, ids, context=None):
223 task_obj = self.pool.get('project.task')
224 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
225 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
226 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
229 def set_pending(self, cr, uid, ids, context=None):
230 self.write(cr, uid, ids, {'state':'pending'}, context=context)
233 def set_open(self, cr, uid, ids, context=None):
234 self.write(cr, uid, ids, {'state':'open'}, context=context)
237 def reset_project(self, cr, uid, ids, context=None):
238 res = self.setActive(cr, uid, ids, value=True, context=context)
239 for (id, name) in self.name_get(cr, uid, ids):
240 message = _('Project ') + " '" + name + "' "+ _("is Open.")
241 self.log(cr, uid, id, message)
244 def copy(self, cr, uid, id, default={}, context=None):
248 task_obj = self.pool.get('project.task')
249 proj = self.browse(cr, uid, id, context=context)
250 default = default or {}
251 context['active_test'] = False
252 default['state'] = 'open'
253 if not default.get('name', False):
254 default['name'] = proj.name+_(' (copy)')
255 res = super(project, self).copy(cr, uid, id, default, context)
259 def duplicate_template(self, cr, uid, ids, context=None):
262 project_obj = self.pool.get('project.project')
263 data_obj = self.pool.get('ir.model.data')
264 task_obj = self.pool.get('project.task')
266 for proj in self.browse(cr, uid, ids, context=context):
267 parent_id = context.get('parent_id', False)
268 context.update({'analytic_project_copy': True})
269 new_date_start = time.strftime('%Y-%m-%d')
271 if proj.date_start and proj.date:
272 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
273 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
274 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
275 new_id = project_obj.copy(cr, uid, proj.id, default = {
276 'name': proj.name +_(' (copy)'),
278 'date_start':new_date_start,
280 'parent_id':parent_id}, context=context)
281 result.append(new_id)
282 cr.execute('select id from project_task where project_id=%s', (proj.id,))
284 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
285 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
287 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
289 if result and len(result):
291 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
292 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
293 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
294 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
295 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
296 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
298 'name': _('Projects'),
300 'view_mode': 'form,tree',
301 'res_model': 'project.project',
304 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
305 'type': 'ir.actions.act_window',
306 'search_view_id': search_view['res_id'],
310 # set active value for a project, its sub projects and its tasks
311 def setActive(self, cr, uid, ids, value=True, context=None):
312 task_obj = self.pool.get('project.task')
313 for proj in self.browse(cr, uid, ids, context=None):
314 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
315 cr.execute('select id from project_task where project_id=%s', (proj.id,))
316 tasks_id = [x[0] for x in cr.fetchall()]
318 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
319 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
321 self.setActive(cr, uid, child_ids, value, context=None)
326 class users(osv.osv):
327 _inherit = 'res.users'
329 'context_project_id': fields.many2one('project.project', 'Project')
334 _name = "project.task"
335 _description = "Task"
337 _date_name = "date_start"
339 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
340 obj_project = self.pool.get('project.project')
342 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
343 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
344 if id and isinstance(id, (long, int)):
345 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
346 args.append(('active', '=', False))
347 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
349 def _str_get(self, task, level=0, border='***', context=None):
350 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'+ \
351 border[0]+' '+(task.name or '')+'\n'+ \
352 (task.description or '')+'\n\n'
354 # Compute: effective_hours, total_hours, progress
355 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
356 project_obj = self.pool.get('project.project')
358 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
359 hours = dict(cr.fetchall())
361 uom_obj = self.pool.get('product.uom')
362 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
363 if user_uom != default_uom:
364 for task in self.browse(cr, uid, ids, context=context):
365 if hours.get(task.id, False):
366 dur_in_user_uom = uom_obj._compute_qty(cr, uid, default_uom, hours.get(task.id, 0.0), user_uom)
367 hours[task.id] = dur_in_user_uom
369 for task in self.browse(cr, uid, ids, context=context):
370 res[task.id] = {'effective_hours': hours.get(task.id, 0.0), 'total_hours': task.remaining_hours + hours.get(task.id, 0.0)}
371 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
372 res[task.id]['progress'] = 0.0
373 if (task.remaining_hours + hours.get(task.id, 0.0)):
374 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
375 if task.state in ('done','cancelled'):
376 res[task.id]['progress'] = 100.0
380 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
381 return {'value':{'remaining_hours': planned - effective}}
383 def _default_project(self, cr, uid, context=None):
386 if 'project_id' in context and context['project_id']:
387 return int(context['project_id'])
390 def copy_data(self, cr, uid, id, default={}, context=None):
391 default = default or {}
392 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
393 if not default.get('remaining_hours', False):
394 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
395 default['active'] = True
396 return super(task, self).copy_data(cr, uid, id, default, context)
398 def _check_dates(self, cr, uid, ids, context=None):
399 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
400 if task['date_start'] and task['date_end']:
401 if task['date_start'] > task['date_end']:
405 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
407 for task in self.browse(cr, uid, ids, context=context):
410 if task.project_id.active == False or task.project_id.state == 'template':
415 '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."),
416 'name': fields.char('Task Summary', size=128, required=True),
417 'description': fields.text('Description'),
418 'priority' : fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Urgent'), ('0','Very urgent')], 'Importance'),
419 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
420 'type_id': fields.many2one('project.task.type', 'Stage',),
421 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
422 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.\
423 \n If the task is over, the states is set to \'Done\'.'),
424 'create_date': fields.datetime('Create Date', readonly=True),
425 'date_start': fields.datetime('Starting Date'),
426 'date_end': fields.datetime('Ending Date'),
427 'date_deadline': fields.date('Deadline'),
428 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade',
429 help="If you have [?] in the project name, it means there are no analytic account linked to this project."),
430 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
431 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
432 'notes': fields.text('Notes'),
433 '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.'),
434 '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."),
435 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
436 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', store=True, help="Computed as: Time Spent + Remaining Time."),
437 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", store=True, help="Computed as: Time Spent / Total Time."),
438 '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."),
440 'user_id': fields.many2one('res.users', 'Assigned to'),
441 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
442 'partner_id': fields.many2one('res.partner', 'Partner'),
443 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
444 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
445 'company_id': fields.many2one('res.company', 'Company'),
454 'project_id': _default_project,
455 'user_id': lambda obj, cr, uid, context: uid,
456 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
459 _order = "sequence, priority, date_start, id"
461 def _check_recursion(self, cr, uid, ids):
462 obj_task = self.browse(cr, uid, ids[0])
463 parent_ids = [x.id for x in obj_task.parent_ids]
464 children_ids = [x.id for x in obj_task.child_ids]
466 if (obj_task.id in children_ids) or (obj_task.id in parent_ids):
470 cr.execute('SELECT DISTINCT task_id '\
471 'FROM project_task_parent_rel '\
472 'WHERE parent_id IN %s', (tuple(ids),))
473 child_ids = map(lambda x: x[0], cr.fetchall())
475 if (list(set(parent_ids).intersection(set(c_ids)))) or (obj_task.id in c_ids):
478 s_ids = self.search(cr, uid, [('parent_ids', 'in', c_ids)])
479 if (list(set(parent_ids).intersection(set(s_ids)))):
486 (_check_dates, 'Error! Task start-date must be lower then task end-date.', ['date_start', 'date_end']),
487 (_check_recursion, _('Error ! You cannot create recursive tasks.'), ['parent_ids'])
490 # Override view according to the company definition
493 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
494 users_obj = self.pool.get('res.users')
495 obj_tm = users_obj.browse(cr, uid, uid, context).company_id.project_time_mode_id
496 tm = obj_tm and obj_tm.name or 'Hours'
498 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
500 if tm in ['Hours','Hour']:
503 eview = etree.fromstring(res['arch'])
505 def _check_rec(eview):
506 if eview.attrib.get('widget','') == 'float_time':
507 eview.set('widget','float')
514 res['arch'] = etree.tostring(eview)
516 for f in res['fields']:
517 if 'Hours' in res['fields'][f]['string']:
518 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
521 def do_close(self, cr, uid, ids, context=None):
527 request = self.pool.get('res.request')
528 for task in self.browse(cr, uid, ids, context=context):
529 project = task.project_id
531 # Send request to project manager
532 if project.warn_manager and project.user_id and (project.user_id.id != uid):
533 request.create(cr, uid, {
534 'name': _("Task '%s' closed") % task.name,
537 'act_to': project.user_id.id,
538 'ref_partner_id': task.partner_id.id,
539 'ref_doc1': 'project.task,%d'% (task.id,),
540 'ref_doc2': 'project.project,%d'% (project.id,),
543 for parent_id in task.parent_ids:
544 if parent_id.state in ('pending','draft'):
546 for child in parent_id.child_ids:
547 if child.id != task.id and child.state not in ('done','cancelled'):
550 self.do_reopen(cr, uid, [parent_id.id])
551 self.write(cr, uid, [task.id], {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
552 message = _('Task ') + " '" + task.name + "' "+ _("is Done.")
553 self.log(cr, uid, task.id, message)
556 def do_reopen(self, cr, uid, ids, context=None):
559 request = self.pool.get('res.request')
560 for task in self.browse(cr, uid, ids, context=context):
561 project = task.project_id
562 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
563 request.create(cr, uid, {
564 'name': _("Task '%s' set in progress") % task.name,
567 'act_to': project.user_id.id,
568 'ref_partner_id': task.partner_id.id,
569 'ref_doc1': 'project.task,%d' % task.id,
570 'ref_doc2': 'project.project,%d' % project.id,
573 self.write(cr, uid, [task.id], {'state': 'open'})
576 def do_cancel(self, cr, uid, ids, *args):
577 request = self.pool.get('res.request')
578 tasks = self.browse(cr, uid, ids)
580 project = task.project_id
581 if project.warn_manager and project.user_id and (project.user_id.id != uid):
582 request.create(cr, uid, {
583 'name': _("Task '%s' cancelled") % task.name,
586 'act_to': project.user_id.id,
587 'ref_partner_id': task.partner_id.id,
588 'ref_doc1': 'project.task,%d' % task.id,
589 'ref_doc2': 'project.project,%d' % project.id,
591 message = _('Task ') + " '" + task.name + "' "+ _("is Cancelled.")
592 self.log(cr, uid, task.id, message)
593 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
596 def do_open(self, cr, uid, ids, *args):
597 tasks= self.browse(cr,uid,ids)
599 self.write(cr, uid, [t.id], {'state': 'open', 'date_start': time.strftime('%Y-%m-%d %H:%M:%S'),})
600 message = _('Task ') + " '" + t.name + "' "+ _("is Open.")
601 self.log(cr, uid, t.id, message)
604 def do_draft(self, cr, uid, ids, *args):
605 self.write(cr, uid, ids, {'state': 'draft'})
608 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
610 Delegate Task to another users.
614 task = self.browse(cr, uid, task_id, context=context)
615 new_task_id = self.copy(cr, uid, task.id, {
616 'name': delegate_data['name'],
617 'user_id': delegate_data['user_id'],
618 'planned_hours': delegate_data['planned_hours'],
619 'remaining_hours': delegate_data['planned_hours'],
620 'parent_ids': [(6, 0, [task.id])],
622 'description': delegate_data['new_task_description'] or '',
626 newname = delegate_data['prefix'] or ''
627 self.write(cr, uid, [task.id], {
628 'remaining_hours': delegate_data['planned_hours_me'],
629 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
632 if delegate_data['state'] == 'pending':
633 self.do_pending(cr, uid, [task.id], context)
635 self.do_close(cr, uid, [task.id], context)
636 user_pool = self.pool.get('res.users')
637 delegrate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
638 message = _('Task ') + " '" + delegate_data['name'] + "' "+ _("is Delegated to User:") +" '"+ delegrate_user.name +"' "
639 self.log(cr, uid, task.id, message)
642 def do_pending(self, cr, uid, ids, *args):
643 self.write(cr, uid, ids, {'state': 'pending'})
644 for (id, name) in self.name_get(cr, uid, ids):
645 message = _('Task ') + " '" + name + "' "+ _("is Pending.")
646 self.log(cr, uid, id, message)
649 def next_type(self, cr, uid, ids, *args):
650 for task in self.browse(cr, uid, ids):
651 typeid = task.type_id.id
652 types = map(lambda x:x.id, task.project_id.type_ids or [])
655 self.write(cr, uid, task.id, {'type_id': types[0]})
656 elif typeid and typeid in types and types.index(typeid) != len(types)-1 :
657 index = types.index(typeid)
658 self.write(cr, uid, task.id, {'type_id': types[index+1]})
661 def prev_type(self, cr, uid, ids, *args):
662 for task in self.browse(cr, uid, ids):
663 typeid = task.type_id.id
664 types = map(lambda x:x.id, task.project_id.type_ids)
666 if typeid and typeid in types:
667 index = types.index(typeid)
668 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
673 class project_work(osv.osv):
674 _name = "project.task.work"
675 _description = "Project Task Work"
677 'name': fields.char('Work summary', size=128),
678 'date': fields.datetime('Date'),
679 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
680 'hours': fields.float('Time Spent'),
681 'user_id': fields.many2one('res.users', 'Done by', required=True),
682 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True)
686 'user_id': lambda obj, cr, uid, context: uid,
687 'date': time.strftime('%Y-%m-%d %H:%M:%S')
692 def create(self, cr, uid, vals, *args, **kwargs):
693 project_obj = self.pool.get('project.project')
694 uom_obj = self.pool.get('product.uom')
695 if vals.get('hours', False):
696 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
697 duration = vals['hours']
698 if user_uom != default_uom:
699 duration = uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
700 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (duration, vals['task_id']))
701 return super(project_work, self).create(cr, uid, vals, *args, **kwargs)
703 def write(self, cr, uid, ids, vals, context=None):
704 project_obj = self.pool.get('project.project')
705 uom_obj = self.pool.get('product.uom')
706 if vals.get('hours', False):
707 old_hours = self.browse(cr, uid, ids, context=context)
708 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
709 duration = vals['hours']
710 for old in old_hours:
711 if vals.get('hours') != old.hours:
712 # this code is only needed when we update the hours of the project
713 # TODO: it may still a second calculation if the task.id is changed
715 if user_uom == default_uom:
716 for work in self.browse(cr, uid, ids, context=context):
717 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, work.hours, work.task_id.id))
719 for work in self.browse(cr, uid, ids, context=context):
720 duration = uom_obj._compute_qty(cr, uid, default_uom, duration, user_uom)
721 del_work = uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
722 cr.execute('update project_task set remaining_hours=remaining_hours - %s + (%s) where id=%s', (duration, del_work, work.task_id.id))
723 return super(project_work,self).write(cr, uid, ids, vals, context=context)
725 def unlink(self, cr, uid, ids, *args, **kwargs):
726 context = kwargs.get('context', {})
727 project_obj = self.pool.get('project.project')
728 uom_obj = self.pool.get('product.uom')
729 user_uom, default_uom = project_obj._get_user_and_default_uom_ids(cr, uid)
730 if user_uom == default_uom:
731 for work in self.browse(cr, uid, ids, context):
732 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
734 for work in self.browse(cr, uid, ids, context):
735 duration = uom_obj._compute_qty(cr, uid, default_uom, work.hours, user_uom)
736 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (duration, work.task_id.id))
737 return super(project_work, self).unlink(cr, uid, ids, *args, **kwargs)
741 class account_analytic_account(osv.osv):
743 _inherit = 'account.analytic.account'
744 _description = 'Analytic Account'
746 def create(self, cr, uid, vals, context=None):
749 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
750 vals['child_ids'] = []
751 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
753 account_analytic_account()
755 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: