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
29 class project_project(osv.osv):
30 _name = 'project.project'
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'),
42 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'),
51 class project(osv.osv):
52 _name = "project.project"
53 _description = "Project"
54 _inherits = {'account.analytic.account': "analytic_account_id"}
56 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
58 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
59 if context and context.has_key('user_prefence') and context['user_prefence']:
60 cr.execute("""SELECT project.id FROM project_project project
61 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
62 LEFT JOIN project_user_rel rel ON rel.project_id = project.analytic_account_id
63 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
64 return [(r[0]) for r in cr.fetchall()]
65 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
66 context=context, count=count)
68 def _complete_name(self, cr, uid, ids, name, args, context=None):
70 for m in self.browse(cr, uid, ids, context=context):
71 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
74 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
75 partner_obj = self.pool.get('res.partner')
77 return {'value':{'contact_id': False, 'pricelist_id': False}}
78 addr = partner_obj.address_get(cr, uid, [part], ['contact'])
79 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
80 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
81 return {'value':{'contact_id': addr['contact'], 'pricelist_id': pricelist_id}}
83 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
84 res = {}.fromkeys(ids, 0.0)
88 project_id, sum(planned_hours), sum(total_hours), sum(effective_hours), SUM(remaining_hours)
95 project_id''', (tuple(ids),))
96 progress = dict(map(lambda x: (x[0], (x[1],x[2],x[3],x[4])), cr.fetchall()))
97 for project in self.browse(cr, uid, ids, context=context):
98 s = progress.get(project.id, (0.0,0.0,0.0,0.0))
100 'planned_hours': s[0],
101 'effective_hours': s[2],
103 'progress_rate': s[1] and round(100.0*s[2]/s[1],2) or 0.0
107 def _get_project_task(self, cr, uid, ids, context=None):
109 for task in self.pool.get('project.task').browse(cr, uid, ids, context=context):
110 if task.project_id: result[task.project_id.id] = True
113 def _get_project_work(self, cr, uid, ids, context=None):
115 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
116 if work.task_id and work.task_id.project_id: result[work.task_id.project_id.id] = True
119 def unlink(self, cr, uid, ids, *args, **kwargs):
120 for proj in self.browse(cr, uid, ids):
122 raise osv.except_osv(_('Operation Not Permitted !'), _('You can not delete a project with tasks. I suggest you to deactivate it.'))
123 return super(project, self).unlink(cr, uid, ids, *args, **kwargs)
126 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
127 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
128 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
129 '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),
130 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"),
131 '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)]}),
133 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
134 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)]}),
135 'tasks': fields.one2many('project.task', 'project_id', "Project tasks"),
136 'planned_hours': fields.function(_progress_rate, multi="progress", string='Planned Time', help="Sum of planned hours of all tasks related to this project and its child projects.",
138 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
139 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
141 'effective_hours': fields.function(_progress_rate, multi="progress", string='Time Spent', help="Sum of spent hours of all tasks related to this project and its child projects."),
142 'total_hours': fields.function(_progress_rate, multi="progress", string='Total Time', help="Sum of total hours of all tasks related to this project and its child projects.",
144 'project.project': (lambda self, cr, uid, ids, c={}: ids, ['tasks'], 10),
145 'project.task': (_get_project_task, ['planned_hours', 'effective_hours', 'remaining_hours', 'total_hours', 'progress', 'delay_hours','state'], 10),
147 'progress_rate': fields.function(_progress_rate, multi="progress", string='Progress', type='float', group_operator="avg", help="Percent of tasks closed according to the total of tasks todo."),
148 '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)]}),
149 '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)]}),
150 '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)]}),
151 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
160 # TODO: Why not using a SQL contraints ?
161 def _check_dates(self, cr, uid, ids, context=None):
162 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
163 if leave['date_start'] and leave['date']:
164 if leave['date_start'] > leave['date']:
169 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
172 def set_template(self, cr, uid, ids, context=None):
173 res = self.setActive(cr, uid, ids, value=False, context=context)
176 def set_done(self, cr, uid, ids, context=None):
177 task_obj = self.pool.get('project.task')
178 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
179 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
180 self.write(cr, uid, ids, {'state':'close'}, context=context)
181 for (id, name) in self.name_get(cr, uid, ids):
182 message = _("The project '%s' has been closed.") % name
183 self.log(cr, uid, id, message)
186 def set_cancel(self, cr, uid, ids, context=None):
187 task_obj = self.pool.get('project.task')
188 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
189 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
190 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
193 def set_pending(self, cr, uid, ids, context=None):
194 self.write(cr, uid, ids, {'state':'pending'}, context=context)
197 def set_open(self, cr, uid, ids, context=None):
198 self.write(cr, uid, ids, {'state':'open'}, context=context)
201 def reset_project(self, cr, uid, ids, context=None):
202 res = self.setActive(cr, uid, ids, value=True, context=context)
203 for (id, name) in self.name_get(cr, uid, ids):
204 message = _("The project '%s' has been opened.") % name
205 self.log(cr, uid, id, message)
208 def copy(self, cr, uid, id, default={}, context=None):
212 default = default or {}
213 context['active_test'] = False
214 default['state'] = 'open'
215 proj = self.browse(cr, uid, id, context=context)
216 if not default.get('name', False):
217 default['name'] = proj.name + _(' (copy)')
219 res = super(project, self).copy(cr, uid, id, default, context)
223 def template_copy(self, cr, uid, id, default={}, context=None):
224 task_obj = self.pool.get('project.task')
225 proj = self.browse(cr, uid, id, context=context)
227 default['tasks'] = [] #avoid to copy all the task automaticly
228 res = self.copy(cr, uid, id, default=default, context=context)
230 #copy all the task manually
232 for task in proj.tasks:
233 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
235 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
236 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
240 def duplicate_template(self, cr, uid, ids, context=None):
243 data_obj = self.pool.get('ir.model.data')
245 for proj in self.browse(cr, uid, ids, context=context):
246 parent_id = context.get('parent_id', False)
247 context.update({'analytic_project_copy': True})
248 new_date_start = time.strftime('%Y-%m-%d')
250 if proj.date_start and proj.date:
251 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
252 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
253 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
254 context.update({'copy':True})
255 new_id = self.template_copy(cr, uid, proj.id, default = {
256 'name': proj.name +_(' (copy)'),
258 'date_start':new_date_start,
260 'parent_id':parent_id}, context=context)
261 result.append(new_id)
263 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
264 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
266 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
268 if result and len(result):
270 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
271 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
272 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
273 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
274 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
275 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
277 'name': _('Projects'),
279 'view_mode': 'form,tree',
280 'res_model': 'project.project',
283 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
284 'type': 'ir.actions.act_window',
285 'search_view_id': search_view['res_id'],
289 # set active value for a project, its sub projects and its tasks
290 def setActive(self, cr, uid, ids, value=True, context=None):
291 task_obj = self.pool.get('project.task')
292 for proj in self.browse(cr, uid, ids, context=None):
293 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
294 cr.execute('select id from project_task where project_id=%s', (proj.id,))
295 tasks_id = [x[0] for x in cr.fetchall()]
297 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
298 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
300 self.setActive(cr, uid, child_ids, value, context=None)
305 class users(osv.osv):
306 _inherit = 'res.users'
308 'context_project_id': fields.many2one('project.project', 'Project')
313 _name = "project.task"
314 _description = "Task"
316 _date_name = "date_start"
318 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
319 obj_project = self.pool.get('project.project')
321 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
322 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
323 if id and isinstance(id, (long, int)):
324 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
325 args.append(('active', '=', False))
326 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
328 def _str_get(self, task, level=0, border='***', context=None):
329 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'+ \
330 border[0]+' '+(task.name or '')+'\n'+ \
331 (task.description or '')+'\n\n'
333 # Compute: effective_hours, total_hours, progress
334 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
336 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
337 hours = dict(cr.fetchall())
338 for task in self.browse(cr, uid, ids, context=context):
339 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)}
340 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
341 res[task.id]['progress'] = 0.0
342 if (task.remaining_hours + hours.get(task.id, 0.0)):
343 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
344 if task.state in ('done','cancelled'):
345 res[task.id]['progress'] = 100.0
349 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
350 if remaining and not planned:
351 return {'value':{'planned_hours': remaining}}
354 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
355 return {'value':{'remaining_hours': planned - effective}}
357 def onchange_project(self, cr, uid, id, project_id):
360 data = self.pool.get('project.project').browse(cr, uid, [project_id])
361 partner_id=data and data[0].parent_id.partner_id
363 return {'value':{'partner_id':partner_id.id}}
366 def _default_project(self, cr, uid, context=None):
369 if 'project_id' in context and context['project_id']:
370 return int(context['project_id'])
373 def duplicate_task(self, cr, uid, map_ids, context=None):
374 for new in map_ids.values():
375 task = self.browse(cr, uid, new, context)
376 child_ids = [ ch.id for ch in task.child_ids]
378 for child in task.child_ids:
379 if child.id in map_ids.keys():
380 child_ids.remove(child.id)
381 child_ids.append(map_ids[child.id])
383 parent_ids = [ ch.id for ch in task.parent_ids]
385 for parent in task.parent_ids:
386 if parent.id in map_ids.keys():
387 parent_ids.remove(parent.id)
388 parent_ids.append(map_ids[parent.id])
389 #FIXME why there is already the copy and the old one
390 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
392 def copy_data(self, cr, uid, id, default={}, context=None):
393 default = default or {}
394 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
395 if not default.get('remaining_hours', False):
396 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
397 default['active'] = True
398 default['type_id'] = False
399 if not default.get('name', False):
400 default['name'] = self.browse(cr, uid, id, context=context).name or ''
401 if not context.get('copy',False):
402 new_name = _("%s (copy)")%default.get('name','')
403 default.update({'name':new_name})
404 return super(task, self).copy_data(cr, uid, id, default, context)
407 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
409 for task in self.browse(cr, uid, ids, context=context):
412 if task.project_id.active == False or task.project_id.state == 'template':
416 def _get_task(self, cr, uid, ids, context=None):
418 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
419 if work.task_id: result[work.task_id.id] = True
423 'active': fields.function(_is_template, 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."),
424 'name': fields.char('Task Summary', size=128, required=True),
425 'description': fields.text('Description'),
426 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
427 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
428 'type_id': fields.many2one('project.task.type', 'Stage'),
429 'state': fields.selection([('draft', 'Draft'),('open', 'In Progress'),('pending', 'Pending'), ('cancelled', 'Cancelled'), ('done', 'Done')], 'State', readonly=True, required=True,
430 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.\
431 \n If the task is over, the states is set to \'Done\'.'),
432 'create_date': fields.datetime('Create Date', readonly=True,select=True),
433 'date_start': fields.datetime('Starting Date',select=True),
434 'date_end': fields.datetime('Ending Date',select=True),
435 'date_deadline': fields.date('Deadline',select=True),
436 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
437 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
438 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
439 'notes': fields.text('Notes'),
440 '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.'),
441 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
443 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
444 'project.task.work': (_get_task, ['hours'], 10),
446 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
447 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
449 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
450 'project.task.work': (_get_task, ['hours'], 10),
452 'progress': fields.function(_hours_get, string='Progress (%)', multi='hours', group_operator="avg", help="If the task has a progress of 99.99% you should close the task if it's finished or reevaluate the time",
454 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
455 'project.task.work': (_get_task, ['hours'], 10),
457 'delay_hours': fields.function(_hours_get, 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.",
459 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
460 'project.task.work': (_get_task, ['hours'], 10),
462 'user_id': fields.many2one('res.users', 'Assigned to'),
463 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
464 'partner_id': fields.many2one('res.partner', 'Partner'),
465 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
466 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
467 'company_id': fields.many2one('res.company', 'Company'),
468 'id': fields.integer('ID'),
477 'project_id': _default_project,
478 'user_id': lambda obj, cr, uid, context: uid,
479 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
482 _order = "sequence,priority, date_start, name, id"
484 def _check_recursion(self, cr, uid, ids, context=None):
486 visited_branch = set()
488 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
494 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
495 if id in visited_branch: #Cycle
498 if id in visited_node: #Already tested don't work one more time for nothing
501 visited_branch.add(id)
504 #visit child using DFS
505 task = self.browse(cr, uid, id, context=context)
506 for child in task.child_ids:
507 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
511 visited_branch.remove(id)
514 def _check_dates(self, cr, uid, ids, context=None):
517 obj_task = self.browse(cr, uid, ids[0], context=context)
518 start = obj_task.date_start or False
519 end = obj_task.date_end or False
526 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
527 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
530 # Override view according to the company definition
532 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
533 users_obj = self.pool.get('res.users')
535 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
536 # this should be safe (no context passed to avoid side-effects)
537 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
538 tm = obj_tm and obj_tm.name or 'Hours'
540 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
542 if tm in ['Hours','Hour']:
545 eview = etree.fromstring(res['arch'])
547 def _check_rec(eview):
548 if eview.attrib.get('widget','') == 'float_time':
549 eview.set('widget','float')
556 res['arch'] = etree.tostring(eview)
558 for f in res['fields']:
559 if 'Hours' in res['fields'][f]['string']:
560 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
563 def _check_child_task(self, cr, uid, ids, context=None):
566 tasks = self.browse(cr, uid, ids, context=context)
569 for child in task.child_ids:
570 if child.state in ['draft', 'open', 'pending']:
571 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
574 def action_close(self, cr, uid, ids, context=None):
575 # This action open wizard to send email to partner or project manager after close task.
578 task_id = len(ids) and ids[0] or False
579 self._check_child_task(cr, uid, ids, context=context)
580 if not task_id: return False
581 task = self.browse(cr, uid, task_id, context=context)
582 project = task.project_id
583 res = self.do_close(cr, uid, [task_id], context=context)
584 if project.warn_manager or project.warn_customer:
586 'name': _('Send Email after close task'),
589 'res_model': 'project.task.close',
590 'type': 'ir.actions.act_window',
593 'context': {'active_id': task.id}
597 def do_close(self, cr, uid, ids, context={}):
601 request = self.pool.get('res.request')
602 for task in self.browse(cr, uid, ids, context=context):
604 project = task.project_id
606 # Send request to project manager
607 if project.warn_manager and project.user_id and (project.user_id.id != uid):
608 request.create(cr, uid, {
609 'name': _("Task '%s' closed") % task.name,
612 'act_to': project.user_id.id,
613 'ref_partner_id': task.partner_id.id,
614 'ref_doc1': 'project.task,%d'% (task.id,),
615 'ref_doc2': 'project.project,%d'% (project.id,),
618 for parent_id in task.parent_ids:
619 if parent_id.state in ('pending','draft'):
621 for child in parent_id.child_ids:
622 if child.id != task.id and child.state not in ('done','cancelled'):
625 self.do_reopen(cr, uid, [parent_id.id], context=context)
626 vals.update({'state': 'done'})
627 vals.update({'remaining_hours': 0.0})
628 if not task.date_end:
629 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
630 self.write(cr, uid, [task.id],vals, context=context)
631 message = _("The task '%s' is done") % (task.name,)
632 self.log(cr, uid, task.id, message)
635 def do_reopen(self, cr, uid, ids, context=None):
636 request = self.pool.get('res.request')
638 for task in self.browse(cr, uid, ids, context=context):
639 project = task.project_id
640 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
641 request.create(cr, uid, {
642 'name': _("Task '%s' set in progress") % task.name,
645 'act_to': project.user_id.id,
646 'ref_partner_id': task.partner_id.id,
647 'ref_doc1': 'project.task,%d' % task.id,
648 'ref_doc2': 'project.project,%d' % project.id,
651 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
654 def do_cancel(self, cr, uid, ids, context={}):
655 request = self.pool.get('res.request')
656 tasks = self.browse(cr, uid, ids, context=context)
657 self._check_child_task(cr, uid, ids, context=context)
659 project = task.project_id
660 if project.warn_manager and project.user_id and (project.user_id.id != uid):
661 request.create(cr, uid, {
662 'name': _("Task '%s' cancelled") % task.name,
665 'act_to': project.user_id.id,
666 'ref_partner_id': task.partner_id.id,
667 'ref_doc1': 'project.task,%d' % task.id,
668 'ref_doc2': 'project.project,%d' % project.id,
670 message = _("The task '%s' is cancelled.") % (task.name,)
671 self.log(cr, uid, task.id, message)
672 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
675 def do_open(self, cr, uid, ids, context={}):
676 tasks= self.browse(cr, uid, ids, context=context)
678 data = {'state': 'open'}
680 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
681 self.write(cr, uid, [t.id], data, context=context)
682 message = _("The task '%s' is opened.") % (t.name,)
683 self.log(cr, uid, t.id, message)
686 def do_draft(self, cr, uid, ids, context={}):
687 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
690 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
692 Delegate Task to another users.
694 task = self.browse(cr, uid, task_id, context=context)
695 self.copy(cr, uid, task.id, {
696 'name': delegate_data['name'],
697 'user_id': delegate_data['user_id'],
698 'planned_hours': delegate_data['planned_hours'],
699 'remaining_hours': delegate_data['planned_hours'],
700 'parent_ids': [(6, 0, [task.id])],
702 'description': delegate_data['new_task_description'] or '',
706 newname = delegate_data['prefix'] or ''
707 self.write(cr, uid, [task.id], {
708 'remaining_hours': delegate_data['planned_hours_me'],
709 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
712 if delegate_data['state'] == 'pending':
713 self.do_pending(cr, uid, [task.id], context)
715 self.do_close(cr, uid, [task.id], context=context)
716 user_pool = self.pool.get('res.users')
717 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
718 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
719 self.log(cr, uid, task.id, message)
722 def do_pending(self, cr, uid, ids, context={}):
723 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
724 for (id, name) in self.name_get(cr, uid, ids):
725 message = _("The task '%s' is pending.") % name
726 self.log(cr, uid, id, message)
729 def _change_type(self, cr, uid, ids, next, *args):
732 if next is False, go to previous stage
734 for task in self.browse(cr, uid, ids):
735 if task.project_id.type_ids:
736 typeid = task.type_id.id
738 for type in task.project_id.type_ids :
739 types_seq[type.id] = type.sequence
741 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
743 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
744 sorted_types = [x[0] for x in types]
746 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
747 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
748 index = sorted_types.index(typeid)
749 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
752 def next_type(self, cr, uid, ids, *args):
753 return self._change_type(cr, uid, ids, True, *args)
755 def prev_type(self, cr, uid, ids, *args):
756 return self._change_type(cr, uid, ids, False, *args)
758 def unlink(self, cr, uid, ids, context=None):
761 self._check_child_task(cr, uid, ids, context=context)
762 res = super(task, self).unlink(cr, uid, ids, context)
767 class project_work(osv.osv):
768 _name = "project.task.work"
769 _description = "Project Task Work"
771 'name': fields.char('Work summary', size=128),
772 'date': fields.datetime('Date', select="1"),
773 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
774 'hours': fields.float('Time Spent'),
775 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
776 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
780 'user_id': lambda obj, cr, uid, context: uid,
781 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
785 def create(self, cr, uid, vals, *args, **kwargs):
786 if 'hours' in vals and (not vals['hours']):
788 if 'task_id' in vals:
789 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
790 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
792 def write(self, cr, uid, ids, vals, context=None):
793 if 'hours' in vals and (not vals['hours']):
796 for work in self.browse(cr, uid, ids, context=context):
797 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))
798 return super(project_work,self).write(cr, uid, ids, vals, context)
800 def unlink(self, cr, uid, ids, *args, **kwargs):
801 for work in self.browse(cr, uid, ids):
802 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
803 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
806 class account_analytic_account(osv.osv):
808 _inherit = 'account.analytic.account'
809 _description = 'Analytic Account'
811 def create(self, cr, uid, vals, context=None):
814 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
815 vals['child_ids'] = []
816 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
818 def unlink(self, cr, uid, ids, *args, **kwargs):
819 project_obj = self.pool.get('project.project')
820 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
822 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
823 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
825 account_analytic_account()
827 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: