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
26 from tools.translate import _
27 from osv import fields, osv
30 class project_task_type(osv.osv):
31 _name = 'project.task.type'
32 _description = 'Task Stage'
35 'name': fields.char('Stage Name', required=True, size=64, translate=True),
36 'description': fields.text('Description'),
37 'sequence': fields.integer('Sequence'),
46 class project(osv.osv):
47 _name = "project.project"
48 _description = "Project"
49 _inherits = {'account.analytic.account': "analytic_account_id"}
51 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
53 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
54 if context and context.has_key('user_prefence') and context['user_prefence']:
55 cr.execute("""SELECT project.id FROM project_project project
56 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
57 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
58 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
59 return [(r[0]) for r in cr.fetchall()]
60 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
61 context=context, count=count)
63 def _complete_name(self, cr, uid, ids, name, args, context=None):
65 for m in self.browse(cr, uid, ids, context=context):
66 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
69 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
70 partner_obj = self.pool.get('res.partner')
72 return {'value':{'contact_id': False, 'pricelist_id': False}}
73 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
74 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
75 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
76 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
78 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
79 res = {}.fromkeys(ids, 0.0)
84 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
91 project_id''', (tuple(ids),))
92 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
93 for project in self.browse(cr, uid, ids, context=context):
94 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
96 'planned_hours': s[0],
97 'effective_hours': s[2],
99 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
103 def _get_project_task(self, cr, uid, ids, context=None):
105 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
106 if task.project_id: result[task.project_id.id] = True
109 def _get_project_work(self, cr, uid, ids, context=None):
111 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
112 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
115 def unlink(self, cr, uid, ids, *args, **kwargs):
116 for proj in self.browse(cr, uid, ids):
118 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
119 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
122 'complete_name': fields.function(_complete_name, method=True, string="Project Name", type='char', size=250),
123 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
124 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
125 '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),
126 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
127 '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)]}),
129 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
130 help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
131 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
132 '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.",
134 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
135 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
137 '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."),
138 '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.",
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 '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."),
144 '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)]}),
145 '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)]}),
146 '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)]}),
147 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
156 # TODO: Why not using a SQL contraints ?
157 def _check_dates(self, cr, uid, ids, context=None):
158 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
159 if leave['date_start'] and leave['date']:
160 if leave['date_start'] > leave['date']:
165 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
168 def set_template(self, cr, uid, ids, context=None):
169 res = self.setActive(cr, uid, ids, value=False, context=context)
172 def set_done(self, cr, uid, ids, context=None):
173 task_obj = self.pool.get('project.task')
174 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
175 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
176 self.write(cr, uid, ids, {'state':'close'}, context=context)
177 for (id, name) in self.name_get(cr, uid, ids):
178 message = _("The project '%s' has been closed.") % name
179 self.log(cr, uid, id, message)
182 def set_cancel(self, cr, uid, ids, context=None):
183 task_obj = self.pool.get('project.task')
184 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
185 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
186 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
189 def set_pending(self, cr, uid, ids, context=None):
190 self.write(cr, uid, ids, {'state':'pending'}, context=context)
193 def set_open(self, cr, uid, ids, context=None):
194 self.write(cr, uid, ids, {'state':'open'}, context=context)
197 def reset_project(self, cr, uid, ids, context=None):
198 res = self.setActive(cr, uid, ids, value=True, context=context)
199 for (id, name) in self.name_get(cr, uid, ids):
200 message = _("The project '%s' has been opened.") % name
201 self.log(cr, uid, id, message)
204 def copy(self, cr, uid, id, default={}, context=None):
208 default = default or {}
209 context['active_test'] = False
210 default['state'] = 'open'
211 if not default.get('name', False):
212 default['name'] = proj.name + _(' (copy)')
214 res = super(project, self).copy(cr, uid, id, default, context)
218 def template_copy(self, cr, uid, id, default={}, context=None):
219 task_obj = self.pool.get('project.task')
220 proj = self.browse(cr, uid, id, context=context)
222 default['tasks'] = [] #avoid to copy all the task automaticly
223 res = self.copy(cr, uid, id, default=default, context=context)
225 #copy all the task manually
227 for task in proj.tasks:
228 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
230 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
231 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
235 def duplicate_template(self, cr, uid, ids, context=None):
238 task_pool = self.pool.get('project.task')
239 data_obj = self.pool.get('ir.model.data')
241 for proj in self.browse(cr, uid, ids, context=context):
242 parent_id = context.get('parent_id', False)
243 context.update({'analytic_project_copy': True})
244 new_date_start = time.strftime('%Y-%m-%d')
246 if proj.date_start and proj.date:
247 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
248 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
249 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
250 context.update({'copy':True})
251 new_id = self.template_copy(cr, uid, proj.id, default = {
252 'name': proj.name +_(' (copy)'),
254 'date_start':new_date_start,
256 'parent_id':parent_id}, context=context)
257 result.append(new_id)
259 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
260 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
262 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
264 if result and len(result):
266 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
267 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
268 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
269 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
270 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
271 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
273 'name': _('Projects'),
275 'view_mode': 'form,tree',
276 'res_model': 'project.project',
279 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
280 'type': 'ir.actions.act_window',
281 'search_view_id': search_view['res_id'],
285 # set active value for a project, its sub projects and its tasks
286 def setActive(self, cr, uid, ids, value=True, context=None):
287 task_obj = self.pool.get('project.task')
288 for proj in self.browse(cr, uid, ids, context=None):
289 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
290 cr.execute('select id from project_task where project_id=%s', (proj.id,))
291 tasks_id = [x[0] for x in cr.fetchall()]
293 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
294 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
296 self.setActive(cr, uid, child_ids, value, context=None)
301 class users(osv.osv):
302 _inherit = 'res.users'
304 'context_project_id': fields.many2one('project.project', 'Project')
309 _name = "project.task"
310 _description = "Task"
312 _date_name = "date_start"
314 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
315 obj_project = self.pool.get('project.project')
317 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
318 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
319 if id and isinstance(id, (long, int)):
320 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
321 args.append(('active', '=', False))
322 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
324 def _str_get(self, task, level=0, border='***', context=None):
325 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'+ \
326 border[0]+' '+(task.name or '')+'\n'+ \
327 (task.description or '')+'\n\n'
329 # Compute: effective_hours, total_hours, progress
330 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
332 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
333 hours = dict(cr.fetchall())
334 for task in self.browse(cr, uid, ids, context=context):
335 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)}
336 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
337 res[task.id]['progress'] = 0.0
338 if (task.remaining_hours + hours.get(task.id, 0.0)):
339 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
340 if task.state in ('done','cancelled'):
341 res[task.id]['progress'] = 100.0
345 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
346 if remaining and not planned:
347 return {'value':{'planned_hours': remaining}}
350 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
351 return {'value':{'remaining_hours': planned - effective}}
353 def onchange_project(self, cr, uid, id, project_id):
356 data = self.pool.get('project.project').browse(cr, uid, [project_id])
357 partner_id=data and data[0].parent_id.partner_id
359 return {'value':{'partner_id':partner_id.id}}
362 def _default_project(self, cr, uid, context=None):
365 if 'project_id' in context and context['project_id']:
366 return int(context['project_id'])
369 def duplicate_task(self, cr, uid, map_ids, context=None):
370 for new in map_ids.values():
371 task = self.browse(cr, uid, new, context)
372 child_ids = [ ch.id for ch in task.child_ids]
374 for child in task.child_ids:
375 if child.id in map_ids.keys():
376 child_ids.remove(child.id)
377 child_ids.append(map_ids[child.id])
379 parent_ids = [ ch.id for ch in task.parent_ids]
381 for parent in task.parent_ids:
382 if parent.id in map_ids.keys():
383 parent_ids.remove(parent.id)
384 parent_ids.append(map_ids[parent.id])
385 #FIXME why there is already the copy and the old one
386 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
388 def copy_data(self, cr, uid, id, default={}, context=None):
389 default = default or {}
390 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
391 if not default.get('remaining_hours', False):
392 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
393 default['active'] = True
394 default['type_id'] = False
395 if not default.get('name', False):
396 default['name'] = self.browse(cr, uid, id, context=context).name or ''
397 if not context.get('copy',False):
398 new_name = _("%s (copy)")%default.get('name','')
399 default.update({'name':new_name})
400 return super(task, self).copy_data(cr, uid, id, default, context)
402 def _check_dates(self, cr, uid, ids, context=None):
403 task = self.read(cr, uid, ids[0], ['date_start', 'date_end'])
404 if task['date_start'] and task['date_end']:
405 if task['date_start'] > task['date_end']:
409 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
411 for task in self.browse(cr, uid, ids, context=context):
414 if task.project_id.active == False or task.project_id.state == 'template':
418 def _get_task(self, cr, uid, ids, context=None):
420 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
421 if work.task_id: result[work.task_id.id] = True
425 '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."),
426 'name': fields.char('Task Summary', size=128, required=True),
427 'description': fields.text('Description'),
428 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
429 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
430 'type_id': fields.many2one('project.task.type', 'Stage'),
431 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
432 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.\
433 \n If the task is over, the states is set to \'Done\'.'),
434 'create_date': fields.datetime('Create Date', readonly=True,select=True),
435 'date_start': fields.datetime('Starting Date',select=True),
436 'date_end': fields.datetime('Ending Date',select=True),
437 'date_deadline': fields.date('Deadline',select=True),
438 'project_id': fields.many2one('project.project', 'Project', ondelete='cascade'),
439 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
440 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
441 'notes': fields.text('Notes'),
442 '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.'),
443 'effective_hours': fields.function(_hours_get, method=True, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
445 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
446 'project.task.work': (_get_task, ['hours'], 10),
448 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
449 'total_hours': fields.function(_hours_get, method=True, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
451 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
452 'project.task.work': (_get_task, ['hours'], 10),
454 'progress': fields.function(_hours_get, method=True, string='Progress (%)', multi='hours', group_operator="avg", help="Computed as: Time Spent / Total Time.",
456 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
457 'project.task.work': (_get_task, ['hours'], 10),
459 '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.",
461 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
462 'project.task.work': (_get_task, ['hours'], 10),
464 'user_id': fields.many2one('res.users', 'Assigned to'),
465 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
466 'partner_id': fields.many2one('res.partner', 'Partner'),
467 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
468 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
469 'company_id': fields.many2one('res.company', 'Company'),
470 'id': fields.integer('ID'),
479 'project_id': _default_project,
480 'user_id': lambda obj, cr, uid, context: uid,
481 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
484 _order = "sequence,priority, date_start, name, id"
486 def _check_recursion(self, cr, uid, ids, context=None):
488 visited_branch = set()
490 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
496 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
497 if id in visited_branch: #Cycle
500 if id in visited_node: #Already tested don't work one more time for nothing
503 visited_branch.add(id)
506 #visit child using DFS
507 task = self.browse(cr, uid, id, context=context)
508 for child in task.child_ids:
509 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
513 visited_branch.remove(id)
516 def _check_dates(self, cr, uid, ids, context=None):
519 obj_task = self.browse(cr, uid, ids[0], context=context)
520 start = obj_task.date_start or False
521 end = obj_task.date_end or False
528 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
529 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
532 # Override view according to the company definition
536 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
537 users_obj = self.pool.get('res.users')
539 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
540 # this should be safe (no context passed to avoid side-effects)
541 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
542 tm = obj_tm and obj_tm.name or 'Hours'
544 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
546 if tm in ['Hours','Hour']:
549 eview = etree.fromstring(res['arch'])
551 def _check_rec(eview):
552 if eview.attrib.get('widget','') == 'float_time':
553 eview.set('widget','float')
560 res['arch'] = etree.tostring(eview)
562 for f in res['fields']:
563 if 'Hours' in res['fields'][f]['string']:
564 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
567 def action_close(self, cr, uid, ids, context=None):
568 # This action open wizard to send email to partner or project manager after close task.
569 project_id = len(ids) and ids[0] or False
570 if not project_id: return False
571 task = self.browse(cr, uid, project_id, context=context)
572 project = task.project_id
573 res = self.do_close(cr, uid, [project_id], context=context)
574 if project.warn_manager or project.warn_customer:
576 'name': _('Send Email after close task'),
579 'res_model': 'project.task.close',
580 'type': 'ir.actions.act_window',
583 'context': {'active_id': task.id}
587 def do_close(self, cr, uid, ids, context=None):
591 request = self.pool.get('res.request')
592 for task in self.browse(cr, uid, ids, context=context):
594 project = task.project_id
596 # Send request to project manager
597 if project.warn_manager and project.user_id and (project.user_id.id != uid):
598 request.create(cr, uid, {
599 'name': _("Task '%s' closed") % 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,),
608 for parent_id in task.parent_ids:
609 if parent_id.state in ('pending','draft'):
611 for child in parent_id.child_ids:
612 if child.id != task.id and child.state not in ('done','cancelled'):
615 self.do_reopen(cr, uid, [parent_id.id])
616 vals.update({'state': 'done'})
617 vals.update({'remaining_hours': 0.0})
618 if not task.date_end:
619 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
620 self.write(cr, uid, [task.id],vals)
621 message = _("The task '%s' is done") % (task.name,)
622 self.log(cr, uid, task.id, message)
625 def do_reopen(self, cr, uid, ids, context=None):
626 request = self.pool.get('res.request')
628 for task in self.browse(cr, uid, ids, context=context):
629 project = task.project_id
630 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
631 request.create(cr, uid, {
632 'name': _("Task '%s' set in progress") % task.name,
635 'act_to': project.user_id.id,
636 'ref_partner_id': task.partner_id.id,
637 'ref_doc1': 'project.task,%d' % task.id,
638 'ref_doc2': 'project.project,%d' % project.id,
641 self.write(cr, uid, [task.id], {'state': 'open'})
645 def do_cancel(self, cr, uid, ids, *args):
646 request = self.pool.get('res.request')
647 tasks = self.browse(cr, uid, ids)
649 project = task.project_id
650 if project.warn_manager and project.user_id and (project.user_id.id != uid):
651 request.create(cr, uid, {
652 'name': _("Task '%s' cancelled") % task.name,
655 'act_to': project.user_id.id,
656 'ref_partner_id': task.partner_id.id,
657 'ref_doc1': 'project.task,%d' % task.id,
658 'ref_doc2': 'project.project,%d' % project.id,
660 message = _("The task '%s' is cancelled.") % (task.name,)
661 self.log(cr, uid, task.id, message)
662 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0})
665 def do_open(self, cr, uid, ids, *args):
666 tasks= self.browse(cr,uid,ids)
668 data = {'state': 'open'}
670 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
671 self.write(cr, uid, [t.id], data)
672 message = _("The task '%s' is opened.") % (t.name,)
673 self.log(cr, uid, t.id, message)
676 def do_draft(self, cr, uid, ids, *args):
677 self.write(cr, uid, ids, {'state': 'draft'})
680 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
682 Delegate Task to another users.
684 task = self.browse(cr, uid, task_id, context=context)
685 self.copy(cr, uid, task.id, {
686 'name': delegate_data['name'],
687 'user_id': delegate_data['user_id'],
688 'planned_hours': delegate_data['planned_hours'],
689 'remaining_hours': delegate_data['planned_hours'],
690 'parent_ids': [(6, 0, [task.id])],
692 'description': delegate_data['new_task_description'] or '',
696 newname = delegate_data['prefix'] or ''
697 self.write(cr, uid, [task.id], {
698 'remaining_hours': delegate_data['planned_hours_me'],
699 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
702 if delegate_data['state'] == 'pending':
703 self.do_pending(cr, uid, [task.id], context)
705 self.do_close(cr, uid, [task.id], context=context)
706 user_pool = self.pool.get('res.users')
707 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
708 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
709 self.log(cr, uid, task.id, message)
712 def do_pending(self, cr, uid, ids, *args):
713 self.write(cr, uid, ids, {'state': 'pending'})
714 for (id, name) in self.name_get(cr, uid, ids):
715 message = _("The task '%s' is pending.") % name
716 self.log(cr, uid, id, message)
719 def next_type(self, cr, uid, ids, *args):
720 for task in self.browse(cr, uid, ids):
721 typeid = task.type_id.id
722 types = map(lambda x:x.id, task.project_id.type_ids or [])
725 self.write(cr, uid, task.id, {'type_id': types[0]})
726 elif typeid and typeid in types and types.index(typeid) != len(types)-1:
727 index = types.index(typeid)
728 self.write(cr, uid, task.id, {'type_id': types[index+1]})
731 def prev_type(self, cr, uid, ids, *args):
732 for task in self.browse(cr, uid, ids):
733 typeid = task.type_id.id
734 types = map(lambda x:x.id, task.project_id and task.project_id.type_ids or [])
736 if typeid and typeid in types:
737 index = types.index(typeid)
738 self.write(cr, uid, task.id, {'type_id': index and types[index-1] or False})
743 class project_work(osv.osv):
744 _name = "project.task.work"
745 _description = "Project Task Work"
747 'name': fields.char('Work summary', size=128),
748 'date': fields.datetime('Date'),
749 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True),
750 'hours': fields.float('Time Spent'),
751 'user_id': fields.many2one('res.users', 'Done by', required=True),
752 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
756 'user_id': lambda obj, cr, uid, context: uid,
757 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
761 def create(self, cr, uid, vals, *args, **kwargs):
762 if 'hours' in vals and (not vals['hours']):
764 if 'task_id' in vals:
765 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
766 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
768 def write(self, cr, uid, ids, vals, context=None):
769 if 'hours' in vals and (not vals['hours']):
772 for work in self.browse(cr, uid, ids, context=context):
773 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))
774 return super(project_work,self).write(cr, uid, ids, vals, context)
776 def unlink(self, cr, uid, ids, *args, **kwargs):
777 for work in self.browse(cr, uid, ids):
778 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
779 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
782 class account_analytic_account(osv.osv):
784 _inherit = 'account.analytic.account'
785 _description = 'Analytic Account'
787 def create(self, cr, uid, vals, context=None):
790 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
791 vals['child_ids'] = []
792 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
794 account_analytic_account()
796 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: