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 # I think we can remove this in v6.1 since VMT's improvements in the framework ?
30 #class project_project(osv.osv):
31 # _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_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."),
43 '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.get('user_preference'):
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 cannot delete a project containing tasks. I suggest you to desactivate 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)]}),
153 def _get_type_common(self, cr, uid, context):
154 ids = self.pool.get('project.task.type').search(cr, uid, [('project_default','=',1)], context=context)
162 'type_ids': _get_type_common
165 # TODO: Why not using a SQL contraints ?
166 def _check_dates(self, cr, uid, ids, context=None):
167 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
168 if leave['date_start'] and leave['date']:
169 if leave['date_start'] > leave['date']:
174 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
177 def set_template(self, cr, uid, ids, context=None):
178 res = self.setActive(cr, uid, ids, value=False, context=context)
181 def set_done(self, cr, uid, ids, context=None):
182 task_obj = self.pool.get('project.task')
183 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', 'not in', ('cancelled', 'done'))])
184 task_obj.write(cr, uid, task_ids, {'state': 'done', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
185 self.write(cr, uid, ids, {'state':'close'}, context=context)
186 for (id, name) in self.name_get(cr, uid, ids):
187 message = _("The project '%s' has been closed.") % name
188 self.log(cr, uid, id, message)
191 def set_cancel(self, cr, uid, ids, context=None):
192 task_obj = self.pool.get('project.task')
193 task_ids = task_obj.search(cr, uid, [('project_id', 'in', ids), ('state', '!=', 'done')])
194 task_obj.write(cr, uid, task_ids, {'state': 'cancelled', 'date_end':time.strftime('%Y-%m-%d %H:%M:%S'), 'remaining_hours': 0.0})
195 self.write(cr, uid, ids, {'state':'cancelled'}, context=context)
198 def set_pending(self, cr, uid, ids, context=None):
199 self.write(cr, uid, ids, {'state':'pending'}, context=context)
202 def set_open(self, cr, uid, ids, context=None):
203 self.write(cr, uid, ids, {'state':'open'}, context=context)
206 def reset_project(self, cr, uid, ids, context=None):
207 res = self.setActive(cr, uid, ids, value=True, context=context)
208 for (id, name) in self.name_get(cr, uid, ids):
209 message = _("The project '%s' has been opened.") % name
210 self.log(cr, uid, id, message)
213 def copy(self, cr, uid, id, default={}, context=None):
217 default = default or {}
218 context['active_test'] = False
219 default['state'] = 'open'
220 proj = self.browse(cr, uid, id, context=context)
221 if not default.get('name', False):
222 default['name'] = proj.name + _(' (copy)')
224 res = super(project, self).copy(cr, uid, id, default, context)
228 def template_copy(self, cr, uid, id, default={}, context=None):
229 task_obj = self.pool.get('project.task')
230 proj = self.browse(cr, uid, id, context=context)
232 default['tasks'] = [] #avoid to copy all the task automaticly
233 res = self.copy(cr, uid, id, default=default, context=context)
235 #copy all the task manually
237 for task in proj.tasks:
238 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
240 self.write(cr, uid, res, {'tasks':[(6,0, map_task_id.values())]})
241 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
245 def duplicate_template(self, cr, uid, ids, context=None):
248 data_obj = self.pool.get('ir.model.data')
250 for proj in self.browse(cr, uid, ids, context=context):
251 parent_id = context.get('parent_id', False)
252 context.update({'analytic_project_copy': True})
253 new_date_start = time.strftime('%Y-%m-%d')
255 if proj.date_start and proj.date:
256 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
257 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
258 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
259 context.update({'copy':True})
260 new_id = self.template_copy(cr, uid, proj.id, default = {
261 'name': proj.name +_(' (copy)'),
263 'date_start':new_date_start,
265 'parent_id':parent_id}, context=context)
266 result.append(new_id)
268 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
269 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
271 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
273 if result and len(result):
275 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
276 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
277 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
278 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
279 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
280 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
282 'name': _('Projects'),
284 'view_mode': 'form,tree',
285 'res_model': 'project.project',
288 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
289 'type': 'ir.actions.act_window',
290 'search_view_id': search_view['res_id'],
294 # set active value for a project, its sub projects and its tasks
295 def setActive(self, cr, uid, ids, value=True, context=None):
296 task_obj = self.pool.get('project.task')
297 for proj in self.browse(cr, uid, ids, context=None):
298 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
299 cr.execute('select id from project_task where project_id=%s', (proj.id,))
300 tasks_id = [x[0] for x in cr.fetchall()]
302 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
303 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
305 self.setActive(cr, uid, child_ids, value, context=None)
310 class users(osv.osv):
311 _inherit = 'res.users'
313 'context_project_id': fields.many2one('project.project', 'Project')
318 _name = "project.task"
319 _description = "Task"
321 _date_name = "date_start"
323 def _read_group_type_id(self, cr, uid, ids, domain, context=None):
324 context = context or {}
325 stage_obj = self.pool.get('project.task.type')
326 stage_ids = stage_obj.search(cr, uid, ['|',('id','in',ids)] + [('project_default','=',1)], context=context)
327 return stage_obj.name_get(cr, uid, stage_ids, context=context)
329 def _read_group_user_id(self, cr, uid, ids, domain, context={}):
330 context = context or {}
331 if type(context.get('project_id', None)) not in (int, long):
333 proj = self.browse(cr, uid, context['project_id'], context=context)
334 ids += map(lambda x: x.id, proj.members)
335 stage_obj = self.pool.get('res.users')
336 stage_ids = stage_obj.search(cr, uid, [('id','in',ids)], context=context)
337 return stage_obj.name_get(cr, uid, ids, context=context)
340 'type_id': _read_group_type_id,
341 'user_id': _read_group_user_id
345 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
346 obj_project = self.pool.get('project.project')
348 if domain[0] == 'project_id' and (not isinstance(domain[2], str)):
349 id = isinstance(domain[2], list) and domain[2][0] or domain[2]
350 if id and isinstance(id, (long, int)):
351 if obj_project.read(cr, user, id, ['state'])['state'] == 'template':
352 args.append(('active', '=', False))
353 return super(task, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
355 def _str_get(self, task, level=0, border='***', context=None):
356 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'+ \
357 border[0]+' '+(task.name or '')+'\n'+ \
358 (task.description or '')+'\n\n'
360 # Compute: effective_hours, total_hours, progress
361 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
363 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
364 hours = dict(cr.fetchall())
365 for task in self.browse(cr, uid, ids, context=context):
366 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)}
367 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
368 res[task.id]['progress'] = 0.0
369 if (task.remaining_hours + hours.get(task.id, 0.0)):
370 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
371 if task.state in ('done','cancelled'):
372 res[task.id]['progress'] = 100.0
376 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0):
377 if remaining and not planned:
378 return {'value':{'planned_hours': remaining}}
381 def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0):
382 return {'value':{'remaining_hours': planned - effective}}
384 def onchange_project(self, cr, uid, id, project_id):
387 data = self.pool.get('project.project').browse(cr, uid, [project_id])
388 partner_id=data and data[0].parent_id.partner_id
390 return {'value':{'partner_id':partner_id.id}}
393 def _default_project(self, cr, uid, context=None):
396 if 'project_id' in context and context['project_id']:
397 return int(context['project_id'])
400 def duplicate_task(self, cr, uid, map_ids, context=None):
401 for new in map_ids.values():
402 task = self.browse(cr, uid, new, context)
403 child_ids = [ ch.id for ch in task.child_ids]
405 for child in task.child_ids:
406 if child.id in map_ids.keys():
407 child_ids.remove(child.id)
408 child_ids.append(map_ids[child.id])
410 parent_ids = [ ch.id for ch in task.parent_ids]
412 for parent in task.parent_ids:
413 if parent.id in map_ids.keys():
414 parent_ids.remove(parent.id)
415 parent_ids.append(map_ids[parent.id])
416 #FIXME why there is already the copy and the old one
417 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]})
419 def copy_data(self, cr, uid, id, default={}, context=None):
420 default = default or {}
421 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
422 if not default.get('remaining_hours', False):
423 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
424 default['active'] = True
425 default['type_id'] = False
426 if not default.get('name', False):
427 default['name'] = self.browse(cr, uid, id, context=context).name or ''
428 if not context.get('copy',False):
429 new_name = _("%s (copy)")%default.get('name','')
430 default.update({'name':new_name})
431 return super(task, self).copy_data(cr, uid, id, default, context)
434 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
436 for task in self.browse(cr, uid, ids, context=context):
439 if task.project_id.active == False or task.project_id.state == 'template':
443 def _get_task(self, cr, uid, ids, context=None):
445 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
446 if work.task_id: result[work.task_id.id] = True
450 '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."),
451 'name': fields.char('Task Summary', size=128, required=True),
452 'description': fields.text('Description'),
453 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority'),
454 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of tasks."),
455 'type_id': fields.many2one('project.task.type', 'Stage'),
456 'state': fields.selection([('draft', 'New'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True,
457 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.\
458 \n If the task is over, the states is set to \'Done\'.'),
459 'kanban_state': fields.selection([('blocked', 'Blocked'),('normal', 'Normal'),('done', 'Done')], 'Kanban State', readonly=True, required=False),
460 'create_date': fields.datetime('Create Date', readonly=True,select=True),
461 'date_start': fields.datetime('Starting Date',select=True),
462 'date_end': fields.datetime('Ending Date',select=True),
463 'date_deadline': fields.date('Deadline',select=True),
464 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1"),
465 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
466 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
467 'notes': fields.text('Notes'),
468 '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.'),
469 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
471 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
472 'project.task.work': (_get_task, ['hours'], 10),
474 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
475 'total_hours': fields.function(_hours_get, string='Total Hours', multi='hours', help="Computed as: Time Spent + Remaining Time.",
477 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
478 'project.task.work': (_get_task, ['hours'], 10),
480 '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",
482 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
483 'project.task.work': (_get_task, ['hours'], 10),
485 '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.",
487 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
488 'project.task.work': (_get_task, ['hours'], 10),
490 'user_id': fields.many2one('res.users', 'Assigned to'),
491 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
492 'partner_id': fields.many2one('res.partner', 'Partner'),
493 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
494 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
495 'company_id': fields.many2one('res.company', 'Company'),
496 'id': fields.integer('ID', readonly=True),
497 'color': fields.integer('Color Index'),
498 'user_email': fields.related('user_id', 'user_email', type='char', string='User Email', readonly=True),
503 'kanban_state': 'normal',
508 'project_id': _default_project,
509 'user_id': lambda obj, cr, uid, context: uid,
510 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c)
513 _order = "priority, sequence, date_start, name, id"
515 def set_priority(self, cr, uid, ids, priority):
518 return self.write(cr, uid, ids, {'priority' : priority})
520 def set_high_priority(self, cr, uid, ids, *args):
521 """Set task priority to high
523 return self.set_priority(cr, uid, ids, '1')
525 def set_normal_priority(self, cr, uid, ids, *args):
526 """Set task priority to normal
528 return self.set_priority(cr, uid, ids, '3')
530 def _check_recursion(self, cr, uid, ids, context=None):
532 visited_branch = set()
534 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
540 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
541 if id in visited_branch: #Cycle
544 if id in visited_node: #Already tested don't work one more time for nothing
547 visited_branch.add(id)
550 #visit child using DFS
551 task = self.browse(cr, uid, id, context=context)
552 for child in task.child_ids:
553 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
557 visited_branch.remove(id)
560 def _check_dates(self, cr, uid, ids, context=None):
563 obj_task = self.browse(cr, uid, ids[0], context=context)
564 start = obj_task.date_start or False
565 end = obj_task.date_end or False
572 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
573 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
576 # Override view according to the company definition
578 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
579 users_obj = self.pool.get('res.users')
581 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
582 # this should be safe (no context passed to avoid side-effects)
583 obj_tm = users_obj.browse(cr, 1, uid, context=context).company_id.project_time_mode_id
584 tm = obj_tm and obj_tm.name or 'Hours'
586 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
588 if tm in ['Hours','Hour']:
591 eview = etree.fromstring(res['arch'])
593 def _check_rec(eview):
594 if eview.attrib.get('widget','') == 'float_time':
595 eview.set('widget','float')
602 res['arch'] = etree.tostring(eview)
604 for f in res['fields']:
605 if 'Hours' in res['fields'][f]['string']:
606 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
609 def _check_child_task(self, cr, uid, ids, context=None):
612 tasks = self.browse(cr, uid, ids, context=context)
615 for child in task.child_ids:
616 if child.state in ['draft', 'open', 'pending']:
617 raise osv.except_osv(_("Warning !"), _("Child task still open.\nPlease cancel or complete child task first."))
620 def action_close(self, cr, uid, ids, context=None):
621 # This action open wizard to send email to partner or project manager after close task.
624 task_id = len(ids) and ids[0] or False
625 self._check_child_task(cr, uid, ids, context=context)
626 if not task_id: return False
627 task = self.browse(cr, uid, task_id, context=context)
628 project = task.project_id
629 res = self.do_close(cr, uid, [task_id], context=context)
630 if project.warn_manager or project.warn_customer:
632 'name': _('Send Email after close task'),
635 'res_model': 'mail.compose.message',
636 'type': 'ir.actions.act_window',
639 'context': {'active_id': task.id,
640 'active_model': 'project.task'}
644 def do_close(self, cr, uid, ids, context={}):
648 request = self.pool.get('res.request')
649 for task in self.browse(cr, uid, ids, context=context):
651 project = task.project_id
653 # Send request to project manager
654 if project.warn_manager and project.user_id and (project.user_id.id != uid):
655 request.create(cr, uid, {
656 'name': _("Task '%s' closed") % task.name,
659 'act_to': project.user_id.id,
660 'ref_partner_id': task.partner_id.id,
661 'ref_doc1': 'project.task,%d'% (task.id,),
662 'ref_doc2': 'project.project,%d'% (project.id,),
665 for parent_id in task.parent_ids:
666 if parent_id.state in ('pending','draft'):
668 for child in parent_id.child_ids:
669 if child.id != task.id and child.state not in ('done','cancelled'):
672 self.do_reopen(cr, uid, [parent_id.id], context=context)
673 vals.update({'state': 'done'})
674 vals.update({'remaining_hours': 0.0})
675 if not task.date_end:
676 vals.update({ 'date_end':time.strftime('%Y-%m-%d %H:%M:%S')})
677 self.write(cr, uid, [task.id],vals, context=context)
678 message = _("The task '%s' is done") % (task.name,)
679 self.log(cr, uid, task.id, message)
682 def do_reopen(self, cr, uid, ids, context=None):
683 request = self.pool.get('res.request')
685 for task in self.browse(cr, uid, ids, context=context):
686 project = task.project_id
687 if project and project.warn_manager and project.user_id.id and (project.user_id.id != uid):
688 request.create(cr, uid, {
689 'name': _("Task '%s' set in progress") % task.name,
692 'act_to': project.user_id.id,
693 'ref_partner_id': task.partner_id.id,
694 'ref_doc1': 'project.task,%d' % task.id,
695 'ref_doc2': 'project.project,%d' % project.id,
698 self.write(cr, uid, [task.id], {'state': 'open'}, context=context)
701 def do_cancel(self, cr, uid, ids, context={}):
702 request = self.pool.get('res.request')
703 tasks = self.browse(cr, uid, ids, context=context)
704 self._check_child_task(cr, uid, ids, context=context)
706 project = task.project_id
707 if project.warn_manager and project.user_id and (project.user_id.id != uid):
708 request.create(cr, uid, {
709 'name': _("Task '%s' cancelled") % task.name,
712 'act_to': project.user_id.id,
713 'ref_partner_id': task.partner_id.id,
714 'ref_doc1': 'project.task,%d' % task.id,
715 'ref_doc2': 'project.project,%d' % project.id,
717 message = _("The task '%s' is cancelled.") % (task.name,)
718 self.log(cr, uid, task.id, message)
719 self.write(cr, uid, [task.id], {'state': 'cancelled', 'remaining_hours':0.0}, context=context)
722 def do_open(self, cr, uid, ids, context={}):
723 tasks= self.browse(cr, uid, ids, context=context)
725 data = {'state': 'open'}
727 data['date_start'] = time.strftime('%Y-%m-%d %H:%M:%S')
728 self.write(cr, uid, [t.id], data, context=context)
729 message = _("The task '%s' is opened.") % (t.name,)
730 self.log(cr, uid, t.id, message)
733 def do_draft(self, cr, uid, ids, context={}):
734 self.write(cr, uid, ids, {'state': 'draft'}, context=context)
737 def do_delegate(self, cr, uid, task_id, delegate_data={}, context=None):
739 Delegate Task to another users.
741 task = self.browse(cr, uid, task_id, context=context)
742 self.copy(cr, uid, task.id, {
743 'name': delegate_data['name'],
744 'user_id': delegate_data['user_id'],
745 'planned_hours': delegate_data['planned_hours'],
746 'remaining_hours': delegate_data['planned_hours'],
747 'parent_ids': [(6, 0, [task.id])],
749 'description': delegate_data['new_task_description'] or '',
753 newname = delegate_data['prefix'] or ''
754 self.write(cr, uid, [task.id], {
755 'remaining_hours': delegate_data['planned_hours_me'],
756 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
759 if delegate_data['state'] == 'pending':
760 self.do_pending(cr, uid, [task.id], context)
762 self.do_close(cr, uid, [task.id], context=context)
763 user_pool = self.pool.get('res.users')
764 delegate_user = user_pool.browse(cr, uid, delegate_data['user_id'], context=context)
765 message = _("The task '%s' has been delegated to %s.") % (delegate_data['name'], delegate_user.name)
766 self.log(cr, uid, task.id, message)
769 def do_pending(self, cr, uid, ids, context={}):
770 self.write(cr, uid, ids, {'state': 'pending'}, context=context)
771 for (id, name) in self.name_get(cr, uid, ids):
772 message = _("The task '%s' is pending.") % name
773 self.log(cr, uid, id, message)
776 def set_remaining_time_1(self, cr, uid, ids, context=None):
777 self.write(cr, uid, ids, {'remaining_hours': 1.0}, context=context)
780 def set_remaining_time_2(self, cr, uid, ids, context=None):
781 self.write(cr, uid, ids, {'remaining_hours': 2.0}, context=context)
784 def set_remaining_time_5(self, cr, uid, ids, context=None):
785 self.write(cr, uid, ids, {'remaining_hours': 5.0}, context=context)
788 def set_remaining_time_10(self, cr, uid, ids, context=None):
789 self.write(cr, uid, ids, {'remaining_hours': 10.0}, context=context)
792 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
793 self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
795 def set_kanban_state_normal(self, cr, uid, ids, context=None):
796 self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
798 def set_kanban_state_done(self, cr, uid, ids, context=None):
799 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
801 def _change_type(self, cr, uid, ids, next, *args):
804 if next is False, go to previous stage
806 for task in self.browse(cr, uid, ids):
807 if task.project_id.type_ids:
808 typeid = task.type_id.id
810 for type in task.project_id.type_ids :
811 types_seq[type.id] = type.sequence
813 types = sorted(types_seq.items(), lambda x, y: cmp(x[1], y[1]))
815 types = sorted(types_seq.items(), lambda x, y: cmp(y[1], x[1]))
816 sorted_types = [x[0] for x in types]
818 self.write(cr, uid, task.id, {'type_id': sorted_types[0]})
819 elif typeid and typeid in sorted_types and sorted_types.index(typeid) != len(sorted_types)-1:
820 index = sorted_types.index(typeid)
821 self.write(cr, uid, task.id, {'type_id': sorted_types[index+1]})
824 def next_type(self, cr, uid, ids, *args):
825 return self._change_type(cr, uid, ids, True, *args)
827 def prev_type(self, cr, uid, ids, *args):
828 return self._change_type(cr, uid, ids, False, *args)
830 def unlink(self, cr, uid, ids, context=None):
833 self._check_child_task(cr, uid, ids, context=context)
834 res = super(task, self).unlink(cr, uid, ids, context)
839 class project_work(osv.osv):
840 _name = "project.task.work"
841 _description = "Project Task Work"
843 'name': fields.char('Work summary', size=128),
844 'date': fields.datetime('Date', select="1"),
845 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
846 'hours': fields.float('Time Spent'),
847 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
848 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
852 'user_id': lambda obj, cr, uid, context: uid,
853 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
857 def create(self, cr, uid, vals, *args, **kwargs):
858 if 'hours' in vals and (not vals['hours']):
860 if 'task_id' in vals:
861 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
862 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
864 def write(self, cr, uid, ids, vals, context=None):
865 if 'hours' in vals and (not vals['hours']):
868 for work in self.browse(cr, uid, ids, context=context):
869 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))
870 return super(project_work,self).write(cr, uid, ids, vals, context)
872 def unlink(self, cr, uid, ids, *args, **kwargs):
873 for work in self.browse(cr, uid, ids):
874 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
875 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
878 class account_analytic_account(osv.osv):
880 _inherit = 'account.analytic.account'
881 _description = 'Analytic Account'
883 def create(self, cr, uid, vals, context=None):
886 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
887 vals['child_ids'] = []
888 return super(account_analytic_account, self).create(cr, uid, vals, context=context)
890 def unlink(self, cr, uid, ids, *args, **kwargs):
891 project_obj = self.pool.get('project.project')
892 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
894 raise osv.except_osv(_('Warning !'), _('Please delete the project linked with this account first.'))
895 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
897 account_analytic_account()
899 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: