1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-today OpenERP SA (<http://www.openerp.com>)
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 datetime import datetime, date
23 from lxml import etree
26 from openerp import SUPERUSER_ID
27 from openerp import tools
28 from openerp.addons.resource.faces import task as Task
29 from openerp.osv import fields, osv
30 from openerp.tools.translate import _
33 class project_task_type(osv.osv):
34 _name = 'project.task.type'
35 _description = 'Task Stage'
38 'name': fields.char('Stage Name', required=True, size=64, translate=True),
39 'description': fields.text('Description'),
40 'sequence': fields.integer('Sequence'),
41 'case_default': fields.boolean('Default for New Projects',
42 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'),
44 'fold': fields.boolean('Folded in Kanban View',
45 help='This stage is folded in the kanban view when'
46 'there are no records in that stage to display.'),
49 def _get_default_project_ids(self, cr, uid, ctx={}):
50 project_id = self.pool['project.task']._get_default_project_id(cr, uid, context=ctx)
57 'project_ids': _get_default_project_ids,
62 class project(osv.osv):
63 _name = "project.project"
64 _description = "Project"
65 _inherits = {'account.analytic.account': "analytic_account_id",
66 "mail.alias": "alias_id"}
67 _inherit = ['mail.thread', 'ir.needaction_mixin']
69 def _auto_init(self, cr, context=None):
70 """ Installation hook: aliases, project.project """
71 # create aliases for all projects and avoid constraint errors
72 alias_context = dict(context, alias_model_name='project.task')
73 return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(project, self)._auto_init,
74 'project.task', self._columns['alias_id'], 'id', alias_prefix='project+', alias_defaults={'project_id':'id'}, context=alias_context)
76 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
78 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
79 if context and context.get('user_preference'):
80 cr.execute("""SELECT project.id FROM project_project project
81 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
82 LEFT JOIN project_user_rel rel ON rel.project_id = project.id
83 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
84 return [(r[0]) for r in cr.fetchall()]
85 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
86 context=context, count=count)
88 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
89 partner_obj = self.pool.get('res.partner')
93 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
94 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
95 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
96 val['pricelist_id'] = pricelist_id
99 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
100 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
101 project_ids = [task.project_id.id for task in tasks if task.project_id]
102 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
104 def _get_project_and_parents(self, cr, uid, ids, context=None):
105 """ return the project ids and all their parent projects """
109 SELECT DISTINCT parent.id
110 FROM project_project project, project_project parent, account_analytic_account account
111 WHERE project.analytic_account_id = account.id
112 AND parent.analytic_account_id = account.parent_id
115 ids = [t[0] for t in cr.fetchall()]
119 def _get_project_and_children(self, cr, uid, ids, context=None):
120 """ retrieve all children projects of project ids;
121 return a dictionary mapping each project to its parent project (or None)
123 res = dict.fromkeys(ids, None)
126 SELECT project.id, parent.id
127 FROM project_project project, project_project parent, account_analytic_account account
128 WHERE project.analytic_account_id = account.id
129 AND parent.analytic_account_id = account.parent_id
132 dic = dict(cr.fetchall())
137 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
138 child_parent = self._get_project_and_children(cr, uid, ids, context)
139 # compute planned_hours, total_hours, effective_hours specific to each project
141 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
142 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
144 LEFT JOIN project_task_type ON project_task.stage_id = project_task_type.id
145 WHERE project_task.project_id IN %s AND project_task_type.fold = False
147 """, (tuple(child_parent.keys()),))
148 # aggregate results into res
149 res = dict([(id, {'planned_hours':0.0, 'total_hours':0.0, 'effective_hours':0.0}) for id in ids])
150 for id, planned, total, effective in cr.fetchall():
151 # add the values specific to id to all parent projects of id in the result
154 res[id]['planned_hours'] += planned
155 res[id]['total_hours'] += total
156 res[id]['effective_hours'] += effective
157 id = child_parent[id]
158 # compute progress rates
160 if res[id]['total_hours']:
161 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
163 res[id]['progress_rate'] = 0.0
166 def unlink(self, cr, uid, ids, context=None):
168 mail_alias = self.pool.get('mail.alias')
169 for proj in self.browse(cr, uid, ids, context=context):
171 raise osv.except_osv(_('Invalid Action!'),
172 _('You cannot delete a project containing tasks. You can either delete all the project\'s tasks and then delete the project or simply deactivate the project.'))
174 alias_ids.append(proj.alias_id.id)
175 res = super(project, self).unlink(cr, uid, ids, context=context)
176 mail_alias.unlink(cr, uid, alias_ids, context=context)
179 def _get_attached_docs(self, cr, uid, ids, field_name, arg, context):
181 attachment = self.pool.get('ir.attachment')
182 task = self.pool.get('project.task')
184 project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True)
185 task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context)
186 task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True)
187 res[id] = (project_attachments or 0) + (task_attachments or 0)
189 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
191 for tasks in self.browse(cr, uid, ids, context):
192 res[tasks.id] = len(tasks.task_ids)
194 def _get_alias_models(self, cr, uid, context=None):
195 """ Overriden in project_issue to offer more options """
196 return [('project.task', "Tasks")]
198 def _get_visibility_selection(self, cr, uid, context=None):
199 """ Overriden in portal_project to offer more options """
200 return [('public', 'Public project'),
201 ('employees', 'Internal project: all employees can access'),
202 ('followers', 'Private project: followers Only')]
204 def attachment_tree_view(self, cr, uid, ids, context):
205 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
208 '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),
209 '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)]
210 res_id = ids and ids[0] or False
212 'name': _('Attachments'),
214 'res_model': 'ir.attachment',
215 'type': 'ir.actions.act_window',
217 'view_mode': 'kanban,tree,form',
220 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id)
223 # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
224 _alias_models = lambda self, *args, **kwargs: self._get_alias_models(*args, **kwargs)
225 _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs)
228 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
229 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
230 'analytic_account_id': fields.many2one('account.analytic.account', 'Contract/Analytic', 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),
231 'priority': fields.integer('Sequence (deprecated)',
232 deprecated='Will be removed with OpenERP v8; use sequence field instead',
233 help="Gives the sequence order when displaying the list of projects"),
234 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
235 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)]}),
236 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
237 '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.",
239 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
240 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20),
242 '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.",
244 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
245 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20),
247 '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.",
249 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
250 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20),
252 '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.",
254 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
255 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20),
257 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
258 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
259 'task_count': fields.function(_task_count, type='integer', string="Tasks",),
260 'task_ids': fields.one2many('project.task', 'project_id',
261 domain=[('stage_id.fold', '=', False)]),
262 'color': fields.integer('Color Index'),
263 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
264 help="Internal email associated with this project. Incoming emails are automatically synchronized"
265 "with Tasks (or optionally Issues if the Issue Tracker module is installed)."),
266 'alias_model': fields.selection(_alias_models, "Alias Model", select=True, required=True,
267 help="The kind of document created when an email is received on this project's email alias"),
268 'privacy_visibility': fields.selection(_visibility_selection, 'Privacy / Visibility', required=True,
269 help="Holds visibility of the tasks or issues that belong to the current project:\n"
270 "- Public: everybody sees everything; if portal is activated, portal users\n"
271 " see all tasks or issues; if anonymous portal is activated, visitors\n"
272 " see all tasks or issues\n"
273 "- Portal (only available if Portal is installed): employees see everything;\n"
274 " if portal is activated, portal users see the tasks or issues followed by\n"
275 " them or by someone of their company\n"
276 "- Employees Only: employees see all tasks or issues\n"
277 "- Followers Only: employees see only the followed tasks or issues; if portal\n"
278 " is activated, portal users see the followed tasks or issues."),
279 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,),
280 'doc_count': fields.function(
281 _get_attached_docs, string="Number of documents attached", type='integer'
285 def _get_type_common(self, cr, uid, context):
286 ids = self.pool.get('project.task.type').search(cr, uid, [('case_default','=',1)], context=context)
289 _order = "sequence, id"
295 'type_ids': _get_type_common,
296 'alias_model': 'project.task',
297 'privacy_visibility': 'employees',
300 # TODO: Why not using a SQL contraints ?
301 def _check_dates(self, cr, uid, ids, context=None):
302 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
303 if leave['date_start'] and leave['date']:
304 if leave['date_start'] > leave['date']:
309 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
312 def set_template(self, cr, uid, ids, context=None):
313 return self.setActive(cr, uid, ids, value=False, context=context)
315 def set_done(self, cr, uid, ids, context=None):
316 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
318 def set_cancel(self, cr, uid, ids, context=None):
319 return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
321 def set_pending(self, cr, uid, ids, context=None):
322 return self.write(cr, uid, ids, {'state': 'pending'}, context=context)
324 def set_open(self, cr, uid, ids, context=None):
325 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
327 def reset_project(self, cr, uid, ids, context=None):
328 return self.setActive(cr, uid, ids, value=True, context=context)
330 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
331 """ copy and map tasks from old to new project """
335 task_obj = self.pool.get('project.task')
336 proj = self.browse(cr, uid, old_project_id, context=context)
337 for task in proj.tasks:
338 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
339 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
340 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
343 def copy(self, cr, uid, id, default=None, context=None):
349 context['active_test'] = False
350 default['state'] = 'open'
351 default['line_ids'] = []
352 default['tasks'] = []
354 # Don't prepare (expensive) data to copy children (analytic accounts),
355 # they are discarded in analytic.copy(), and handled in duplicate_template()
356 default['child_ids'] = []
358 proj = self.browse(cr, uid, id, context=context)
359 if not default.get('name', False):
360 default.update(name=_("%s (copy)") % (proj.name))
361 res = super(project, self).copy(cr, uid, id, default, context)
362 self.map_tasks(cr, uid, id, res, context=context)
365 def duplicate_template(self, cr, uid, ids, context=None):
368 data_obj = self.pool.get('ir.model.data')
370 for proj in self.browse(cr, uid, ids, context=context):
371 parent_id = context.get('parent_id', False)
372 context.update({'analytic_project_copy': True})
373 new_date_start = time.strftime('%Y-%m-%d')
375 if proj.date_start and proj.date:
376 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
377 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
378 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
379 context.update({'copy':True})
380 new_id = self.copy(cr, uid, proj.id, default = {
381 'name':_("%s (copy)") % (proj.name),
383 'date_start':new_date_start,
385 'parent_id':parent_id}, context=context)
386 result.append(new_id)
388 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
389 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
391 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
393 if result and len(result):
395 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
396 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
397 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
398 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
399 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
400 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
402 'name': _('Projects'),
404 'view_mode': 'form,tree',
405 'res_model': 'project.project',
408 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
409 'type': 'ir.actions.act_window',
410 'search_view_id': search_view['res_id'],
414 # set active value for a project, its sub projects and its tasks
415 def setActive(self, cr, uid, ids, value=True, context=None):
416 task_obj = self.pool.get('project.task')
417 for proj in self.browse(cr, uid, ids, context=None):
418 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
419 cr.execute('select id from project_task where project_id=%s', (proj.id,))
420 tasks_id = [x[0] for x in cr.fetchall()]
422 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
423 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
425 self.setActive(cr, uid, child_ids, value, context=None)
428 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
429 context = context or {}
430 if type(ids) in (long, int,):
432 projects = self.browse(cr, uid, ids, context=context)
434 for project in projects:
435 if (not project.members) and force_members:
436 raise osv.except_osv(_('Warning!'),_("You must assign members on the project '%s'!") % (project.name,))
438 resource_pool = self.pool.get('resource.resource')
440 result = "from openerp.addons.resource.faces import *\n"
441 result += "import datetime\n"
442 for project in self.browse(cr, uid, ids, context=context):
443 u_ids = [i.id for i in project.members]
444 if project.user_id and (project.user_id.id not in u_ids):
445 u_ids.append(project.user_id.id)
446 for task in project.tasks:
447 if task.user_id and (task.user_id.id not in u_ids):
448 u_ids.append(task.user_id.id)
449 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
450 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
451 for key, vals in resource_objs.items():
453 class User_%s(Resource):
455 ''' % (key, vals.get('efficiency', False))
462 def _schedule_project(self, cr, uid, project, context=None):
463 resource_pool = self.pool.get('resource.resource')
464 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
465 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
466 # TODO: check if we need working_..., default values are ok.
467 puids = [x.id for x in project.members]
469 puids.append(project.user_id.id)
477 project.date_start or time.strftime('%Y-%m-%d'), working_days,
478 '|'.join(['User_'+str(x) for x in puids]) or 'None'
480 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
487 #TODO: DO Resource allocation and compute availability
488 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
494 def schedule_tasks(self, cr, uid, ids, context=None):
495 context = context or {}
496 if type(ids) in (long, int,):
498 projects = self.browse(cr, uid, ids, context=context)
499 result = self._schedule_header(cr, uid, ids, False, context=context)
500 for project in projects:
501 result += self._schedule_project(cr, uid, project, context=context)
502 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
505 exec result in local_dict
506 projects_gantt = Task.BalancedProject(local_dict['Project'])
508 for project in projects:
509 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
510 for task in project.tasks:
511 if task.stage_id and task.stage_id.fold:
514 p = getattr(project_gantt, 'Task_%d' % (task.id,))
516 self.pool.get('project.task').write(cr, uid, [task.id], {
517 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
518 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
520 if (not task.user_id) and (p.booked_resource):
521 self.pool.get('project.task').write(cr, uid, [task.id], {
522 'user_id': int(p.booked_resource[0].name[5:]),
526 def create(self, cr, uid, vals, context=None):
529 # Prevent double project creation when 'use_tasks' is checked + alias management
530 create_context = dict(context, project_creation_in_progress=True,
531 alias_model_name=vals.get('alias_model', 'project.task'),
532 alias_parent_model_name=self._name)
534 if vals.get('type', False) not in ('template', 'contract'):
535 vals['type'] = 'contract'
537 project_id = super(project, self).create(cr, uid, vals, context=create_context)
538 project_rec = self.browse(cr, uid, project_id, context=context)
539 self.pool.get('mail.alias').write(cr, uid, [project_rec.alias_id.id], {'alias_parent_thread_id': project_id, 'alias_defaults': {'project_id': project_id}}, context)
542 def write(self, cr, uid, ids, vals, context=None):
543 # if alias_model has been changed, update alias_model_id accordingly
544 if vals.get('alias_model'):
545 model_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', vals.get('alias_model', 'project.task'))])
546 vals.update(alias_model_id=model_ids[0])
547 return super(project, self).write(cr, uid, ids, vals, context=context)
551 _name = "project.task"
552 _description = "Task"
553 _date_name = "date_start"
554 _inherit = ['mail.thread', 'ir.needaction_mixin']
556 _mail_post_access = 'read'
559 # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
560 'project.mt_task_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence <= 1,
561 'project.mt_task_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id.sequence > 1,
564 'project.mt_task_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id,
567 'project.mt_task_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked',
568 'project.mt_task_ready': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'done',
572 def _get_default_partner(self, cr, uid, context=None):
573 project_id = self._get_default_project_id(cr, uid, context)
575 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
576 if project and project.partner_id:
577 return project.partner_id.id
580 def _get_default_project_id(self, cr, uid, context=None):
581 """ Gives default section by checking if present in the context """
582 return (self._resolve_project_id_from_context(cr, uid, context=context) or False)
584 def _get_default_stage_id(self, cr, uid, context=None):
585 """ Gives default stage_id """
586 project_id = self._get_default_project_id(cr, uid, context=context)
587 return self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context)
589 def _resolve_project_id_from_context(self, cr, uid, context=None):
590 """ Returns ID of project based on the value of 'default_project_id'
591 context key, or None if it cannot be resolved to a single
596 if type(context.get('default_project_id')) in (int, long):
597 return context['default_project_id']
598 if isinstance(context.get('default_project_id'), basestring):
599 project_name = context['default_project_id']
600 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
601 if len(project_ids) == 1:
602 return project_ids[0][0]
605 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
606 stage_obj = self.pool.get('project.task.type')
607 order = stage_obj._order
608 access_rights_uid = access_rights_uid or uid
609 if read_group_order == 'stage_id desc':
610 order = '%s desc' % order
612 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
614 search_domain += ['|', ('project_ids', '=', project_id)]
615 search_domain += [('id', 'in', ids)]
616 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
617 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
618 # restore order of the search
619 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
622 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
623 fold[stage.id] = stage.fold or False
626 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
627 res_users = self.pool.get('res.users')
628 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
629 access_rights_uid = access_rights_uid or uid
631 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
632 order = res_users._order
633 # lame way to allow reverting search, should just work in the trivial case
634 if read_group_order == 'user_id desc':
635 order = '%s desc' % order
636 # de-duplicate and apply search order
637 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
638 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
639 # restore order of the search
640 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
644 'stage_id': _read_group_stage_ids,
645 'user_id': _read_group_user_id,
648 def _str_get(self, task, level=0, border='***', context=None):
649 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'+ \
650 border[0]+' '+(task.name or '')+'\n'+ \
651 (task.description or '')+'\n\n'
653 # Compute: effective_hours, total_hours, progress
654 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
656 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
657 hours = dict(cr.fetchall())
658 for task in self.browse(cr, uid, ids, context=context):
659 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)}
660 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
661 res[task.id]['progress'] = 0.0
662 if (task.remaining_hours + hours.get(task.id, 0.0)):
663 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
664 # TDE CHECK: if task.state in ('done','cancelled'):
665 if task.stage_id and task.stage_id.fold:
666 res[task.id]['progress'] = 100.0
669 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned=0.0):
670 if remaining and not planned:
671 return {'value': {'planned_hours': remaining}}
674 def onchange_planned(self, cr, uid, ids, planned=0.0, effective=0.0):
675 return {'value': {'remaining_hours': planned - effective}}
677 def onchange_project(self, cr, uid, id, project_id, context=None):
679 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
680 if project and project.partner_id:
681 return {'value': {'partner_id': project.partner_id.id}}
684 def onchange_user_id(self, cr, uid, ids, user_id, context=None):
687 vals['date_start'] = fields.datetime.now()
688 return {'value': vals}
690 def duplicate_task(self, cr, uid, map_ids, context=None):
691 mapper = lambda t: map_ids.get(t.id, t.id)
692 for task in self.browse(cr, uid, map_ids.values(), context):
693 new_child_ids = set(map(mapper, task.child_ids))
694 new_parent_ids = set(map(mapper, task.parent_ids))
695 if new_child_ids or new_parent_ids:
696 task.write({'parent_ids': [(6,0,list(new_parent_ids))],
697 'child_ids': [(6,0,list(new_child_ids))]})
699 def copy_data(self, cr, uid, id, default=None, context=None):
702 default = default or {}
703 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
704 if not default.get('remaining_hours', False):
705 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
706 default['active'] = True
707 if not default.get('name', False):
708 default['name'] = self.browse(cr, uid, id, context=context).name or ''
709 if not context.get('copy',False):
710 new_name = _("%s (copy)") % (default.get('name', ''))
711 default.update({'name':new_name})
712 return super(task, self).copy_data(cr, uid, id, default, context)
714 def copy(self, cr, uid, id, default=None, context=None):
719 if not context.get('copy', False):
720 stage = self._get_default_stage_id(cr, uid, context=context)
722 default['stage_id'] = stage
723 return super(task, self).copy(cr, uid, id, default, context)
725 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
727 for task in self.browse(cr, uid, ids, context=context):
730 if task.project_id.active == False or task.project_id.state == 'template':
734 def _get_task(self, cr, uid, ids, context=None):
736 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
737 if work.task_id: result[work.task_id.id] = True
741 '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."),
742 'name': fields.char('Task Summary', track_visibility='onchange', size=128, required=True, select=True),
743 'description': fields.text('Description'),
744 'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], 'Priority', select=True),
745 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
746 'stage_id': fields.many2one('project.task.type', 'Stage', track_visibility='onchange', select=True,
747 domain="[('project_ids', '=', project_id)]"),
748 'categ_ids': fields.many2many('project.category', string='Tags'),
749 'kanban_state': fields.selection([('normal', 'In Progress'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
750 track_visibility='onchange',
751 help="A task's kanban state indicates special situations affecting it:\n"
752 " * Normal is the default situation\n"
753 " * Blocked indicates something is preventing the progress of this task\n"
754 " * Ready for next stage indicates the task is ready to be pulled to the next stage",
756 'create_date': fields.datetime('Create Date', readonly=True, select=True),
757 'write_date': fields.datetime('Last Modification Date', readonly=True, select=True), #not displayed in the view but it might be useful with base_action_rule module (and it needs to be defined first for that)
758 'date_start': fields.datetime('Starting Date',select=True),
759 'date_end': fields.datetime('Ending Date',select=True),
760 'date_deadline': fields.date('Deadline',select=True),
761 'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
762 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select=True, track_visibility='onchange', change_default=True),
763 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
764 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
765 'notes': fields.text('Notes'),
766 'planned_hours': fields.float('Initially Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'),
767 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
769 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
770 'project.task.work': (_get_task, ['hours'], 10),
772 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
773 'total_hours': fields.function(_hours_get, string='Total', multi='hours', help="Computed as: Time Spent + Remaining Time.",
775 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
776 'project.task.work': (_get_task, ['hours'], 10),
778 'progress': fields.function(_hours_get, string='Working Time 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",
780 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10),
781 'project.task.work': (_get_task, ['hours'], 10),
783 'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='hours', help="Computed as difference between planned hours by the project manager and the total hours of the task.",
785 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
786 'project.task.work': (_get_task, ['hours'], 10),
788 'user_id': fields.many2one('res.users', 'Assigned to', select=True, track_visibility='onchange'),
789 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
790 'partner_id': fields.many2one('res.partner', 'Customer'),
791 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
792 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
793 'company_id': fields.many2one('res.company', 'Company'),
794 'id': fields.integer('ID', readonly=True),
795 'color': fields.integer('Color Index'),
796 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
799 'stage_id': _get_default_stage_id,
800 'project_id': _get_default_project_id,
801 'date_last_stage_update': fields.datetime.now,
802 'kanban_state': 'normal',
807 'user_id': lambda obj, cr, uid, ctx=None: uid,
808 'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=ctx),
809 'partner_id': lambda self, cr, uid, ctx=None: self._get_default_partner(cr, uid, context=ctx),
811 _order = "priority, sequence, date_start, name, id"
813 def _check_recursion(self, cr, uid, ids, context=None):
815 visited_branch = set()
817 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
823 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
824 if id in visited_branch: #Cycle
827 if id in visited_node: #Already tested don't work one more time for nothing
830 visited_branch.add(id)
833 #visit child using DFS
834 task = self.browse(cr, uid, id, context=context)
835 for child in task.child_ids:
836 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
840 visited_branch.remove(id)
843 def _check_dates(self, cr, uid, ids, context=None):
846 obj_task = self.browse(cr, uid, ids[0], context=context)
847 start = obj_task.date_start or False
848 end = obj_task.date_end or False
855 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
856 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
859 # Override view according to the company definition
860 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
861 users_obj = self.pool.get('res.users')
862 if context is None: context = {}
863 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
864 # this should be safe (no context passed to avoid side-effects)
865 obj_tm = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id
866 tm = obj_tm and obj_tm.name or 'Hours'
868 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
870 if tm in ['Hours','Hour']:
873 eview = etree.fromstring(res['arch'])
875 def _check_rec(eview):
876 if eview.attrib.get('widget','') == 'float_time':
877 eview.set('widget','float')
884 res['arch'] = etree.tostring(eview)
886 for f in res['fields']:
887 if 'Hours' in res['fields'][f]['string']:
888 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
891 def get_empty_list_help(self, cr, uid, help, context=None):
892 context['empty_list_help_id'] = context.get('default_project_id')
893 context['empty_list_help_model'] = 'project.project'
894 context['empty_list_help_document_name'] = _("tasks")
895 return super(task, self).get_empty_list_help(cr, uid, help, context=context)
897 # ----------------------------------------
899 # ----------------------------------------
901 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
902 """ Override of the base.stage method
903 Parameter of the stage search taken from the lead:
904 - section_id: if set, stages must belong to this section or
905 be a default stage; if not set, stages must be default
908 if isinstance(cases, (int, long)):
909 cases = self.browse(cr, uid, cases, context=context)
910 # collect all section_ids
913 section_ids.append(section_id)
916 section_ids.append(task.project_id.id)
919 search_domain = [('|')] * (len(section_ids) - 1)
920 for section_id in section_ids:
921 search_domain.append(('project_ids', '=', section_id))
922 search_domain += list(domain)
923 # perform search, return the first found
924 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
929 def _check_child_task(self, cr, uid, ids, context=None):
932 tasks = self.browse(cr, uid, ids, context=context)
935 for child in task.child_ids:
936 if child.stage_id and not child.stage_id.fold:
937 raise osv.except_osv(_("Warning!"), _("Child task still open.\nPlease cancel or complete child task first."))
940 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
941 attachment = self.pool.get('ir.attachment')
942 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
943 new_attachment_ids = []
944 for attachment_id in attachment_ids:
945 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
946 return new_attachment_ids
948 def do_delegate(self, cr, uid, ids, delegate_data=None, context=None):
950 Delegate Task to another users.
952 if delegate_data is None:
954 assert delegate_data['user_id'], _("Delegated User should be specified")
956 for task in self.browse(cr, uid, ids, context=context):
957 delegated_task_id = self.copy(cr, uid, task.id, {
958 'name': delegate_data['name'],
959 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
960 'stage_id': delegate_data.get('stage_id') and delegate_data.get('stage_id')[0] or False,
961 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
962 'planned_hours': delegate_data['planned_hours'] or 0.0,
963 'parent_ids': [(6, 0, [task.id])],
964 'description': delegate_data['new_task_description'] or '',
968 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
969 newname = delegate_data['prefix'] or ''
971 'remaining_hours': delegate_data['planned_hours_me'],
972 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
975 delegated_tasks[task.id] = delegated_task_id
976 return delegated_tasks
978 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
979 for task in self.browse(cr, uid, ids, context=context):
980 if (task.stage_id and task.stage_id.sequence <= 1) or (task.planned_hours == 0.0):
981 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
982 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
985 def set_remaining_time_1(self, cr, uid, ids, context=None):
986 return self.set_remaining_time(cr, uid, ids, 1.0, context)
988 def set_remaining_time_2(self, cr, uid, ids, context=None):
989 return self.set_remaining_time(cr, uid, ids, 2.0, context)
991 def set_remaining_time_5(self, cr, uid, ids, context=None):
992 return self.set_remaining_time(cr, uid, ids, 5.0, context)
994 def set_remaining_time_10(self, cr, uid, ids, context=None):
995 return self.set_remaining_time(cr, uid, ids, 10.0, context)
997 def _store_history(self, cr, uid, ids, context=None):
998 for task in self.browse(cr, uid, ids, context=context):
999 self.pool.get('project.task.history').create(cr, uid, {
1001 'remaining_hours': task.remaining_hours,
1002 'planned_hours': task.planned_hours,
1003 'kanban_state': task.kanban_state,
1004 'type_id': task.stage_id.id,
1005 'user_id': task.user_id.id
1010 # ------------------------------------------------
1012 # ------------------------------------------------
1014 def create(self, cr, uid, vals, context=None):
1019 if vals.get('project_id') and not context.get('default_project_id'):
1020 context['default_project_id'] = vals.get('project_id')
1021 # user_id change: update date_start
1022 if vals.get('user_id') and not vals.get('start_date'):
1023 vals['date_start'] = fields.datetime.now()
1025 # context: no_log, because subtype already handle this
1026 create_context = dict(context, mail_create_nolog=True)
1027 task_id = super(task, self).create(cr, uid, vals, context=create_context)
1028 self._store_history(cr, uid, [task_id], context=context)
1031 def write(self, cr, uid, ids, vals, context=None):
1032 if isinstance(ids, (int, long)):
1035 # stage change: update date_last_stage_update
1036 if 'stage_id' in vals:
1037 vals['date_last_stage_update'] = fields.datetime.now()
1038 # user_id change: update date_start
1039 if vals.get('user_id') and 'date_start' not in vals:
1040 vals['date_start'] = fields.datetime.now()
1042 # Overridden to reset the kanban_state to normal whenever
1043 # the stage (stage_id) of the task changes.
1044 if vals and not 'kanban_state' in vals and 'stage_id' in vals:
1045 new_stage = vals.get('stage_id')
1046 vals_reset_kstate = dict(vals, kanban_state='normal')
1047 for t in self.browse(cr, uid, ids, context=context):
1048 write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
1049 super(task, self).write(cr, uid, [t.id], write_vals, context=context)
1052 result = super(task, self).write(cr, uid, ids, vals, context=context)
1054 if any(item in vals for item in ['stage_id', 'remaining_hours', 'user_id', 'kanban_state']):
1055 self._store_history(cr, uid, ids, context=context)
1058 def unlink(self, cr, uid, ids, context=None):
1061 self._check_child_task(cr, uid, ids, context=context)
1062 res = super(task, self).unlink(cr, uid, ids, context)
1065 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1066 context = context or {}
1070 if task.stage_id and task.stage_id.fold:
1075 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1077 for t2 in task.parent_ids:
1078 start.append("up.Task_%s.end" % (t2.id,))
1082 ''' % (ident,','.join(start))
1087 ''' % (ident, 'User_'+str(task.user_id.id))
1092 # ---------------------------------------------------
1094 # ---------------------------------------------------
1096 def message_get_reply_to(self, cr, uid, ids, context=None):
1097 """ Override to get the reply_to of the parent project. """
1098 return [task.project_id.message_get_reply_to()[0] if task.project_id else False
1099 for task in self.browse(cr, uid, ids, context=context)]
1101 def message_new(self, cr, uid, msg, custom_values=None, context=None):
1102 """ Override to updates the document according to the email. """
1103 if custom_values is None:
1106 'name': msg.get('subject'),
1107 'planned_hours': 0.0,
1109 defaults.update(custom_values)
1110 return super(task, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
1112 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
1113 """ Override to update the task according to the email. """
1114 if update_vals is None:
1117 'cost': 'planned_hours',
1119 for line in msg['body'].split('\n'):
1121 res = tools.command_re.match(line)
1123 match = res.group(1).lower()
1124 field = maps.get(match)
1127 update_vals[field] = float(res.group(2).lower())
1128 except (ValueError, TypeError):
1130 return super(task, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
1132 class project_work(osv.osv):
1133 _name = "project.task.work"
1134 _description = "Project Task Work"
1136 'name': fields.char('Work summary', size=128),
1137 'date': fields.datetime('Date', select="1"),
1138 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1139 'hours': fields.float('Time Spent'),
1140 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1141 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1145 'user_id': lambda obj, cr, uid, context: uid,
1146 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1149 _order = "date desc"
1150 def create(self, cr, uid, vals, *args, **kwargs):
1151 if 'hours' in vals and (not vals['hours']):
1152 vals['hours'] = 0.00
1153 if 'task_id' in vals:
1154 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1155 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1157 def write(self, cr, uid, ids, vals, context=None):
1158 if 'hours' in vals and (not vals['hours']):
1159 vals['hours'] = 0.00
1161 for work in self.browse(cr, uid, ids, context=context):
1162 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))
1163 return super(project_work,self).write(cr, uid, ids, vals, context)
1165 def unlink(self, cr, uid, ids, *args, **kwargs):
1166 for work in self.browse(cr, uid, ids):
1167 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1168 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1171 class account_analytic_account(osv.osv):
1172 _inherit = 'account.analytic.account'
1173 _description = 'Analytic Account'
1175 'use_tasks': fields.boolean('Tasks',help="If checked, this contract will be available in the project menu and you will be able to manage tasks or track issues"),
1176 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
1179 def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
1180 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
1181 if template_id and 'value' in res:
1182 template = self.browse(cr, uid, template_id, context=context)
1183 res['value']['use_tasks'] = template.use_tasks
1186 def _trigger_project_creation(self, cr, uid, vals, context=None):
1188 This function is used to decide if a project needs to be automatically created or not when an analytic account is created. It returns True if it needs to be so, False otherwise.
1190 if context is None: context = {}
1191 return vals.get('use_tasks') and not 'project_creation_in_progress' in context
1193 def project_create(self, cr, uid, analytic_account_id, vals, context=None):
1195 This function is called at the time of analytic account creation and is used to create a project automatically linked to it if the conditions are meet.
1197 project_pool = self.pool.get('project.project')
1198 project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)])
1199 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):
1201 'name': vals.get('name'),
1202 'analytic_account_id': analytic_account_id,
1203 'type': vals.get('type','contract'),
1205 return project_pool.create(cr, uid, project_values, context=context)
1208 def create(self, cr, uid, vals, context=None):
1211 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1212 vals['child_ids'] = []
1213 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
1214 self.project_create(cr, uid, analytic_account_id, vals, context=context)
1215 return analytic_account_id
1217 def write(self, cr, uid, ids, vals, context=None):
1218 if isinstance(ids, (int, long)):
1220 vals_for_project = vals.copy()
1221 for account in self.browse(cr, uid, ids, context=context):
1222 if not vals.get('name'):
1223 vals_for_project['name'] = account.name
1224 if not vals.get('type'):
1225 vals_for_project['type'] = account.type
1226 self.project_create(cr, uid, account.id, vals_for_project, context=context)
1227 return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context)
1229 def unlink(self, cr, uid, ids, *args, **kwargs):
1230 project_obj = self.pool.get('project.project')
1231 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1233 raise osv.except_osv(_('Warning!'), _('Please delete the project linked with this account first.'))
1234 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1236 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
1241 if context.get('current_model') == 'project.project':
1242 project_ids = self.search(cr, uid, args + [('name', operator, name)], limit=limit, context=context)
1243 return self.name_get(cr, uid, project_ids, context=context)
1245 return super(account_analytic_account, self).name_search(cr, uid, name, args=args, operator=operator, context=context, limit=limit)
1248 class project_project(osv.osv):
1249 _inherit = 'project.project'
1254 class project_task_history(osv.osv):
1256 Tasks History, used for cumulative flow charts (Lean/Agile)
1258 _name = 'project.task.history'
1259 _description = 'History of Tasks'
1260 _rec_name = 'task_id'
1263 def _get_date(self, cr, uid, ids, name, arg, context=None):
1265 for history in self.browse(cr, uid, ids, context=context):
1266 if history.type_id and history.type_id.fold:
1267 result[history.id] = history.date
1269 cr.execute('''select
1272 project_task_history
1276 order by id limit 1''', (history.task_id.id, history.id))
1278 result[history.id] = res and res[0] or False
1281 def _get_related_date(self, cr, uid, ids, context=None):
1283 for history in self.browse(cr, uid, ids, context=context):
1284 cr.execute('''select
1287 project_task_history
1291 order by id desc limit 1''', (history.task_id.id, history.id))
1294 result.append(res[0])
1298 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1299 'type_id': fields.many2one('project.task.type', 'Stage'),
1300 'kanban_state': fields.selection([('normal', 'Normal'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], 'Kanban State', required=False),
1301 'date': fields.date('Date', select=True),
1302 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1303 'project.task.history': (_get_related_date, None, 20)
1305 'remaining_hours': fields.float('Remaining Time', digits=(16, 2)),
1306 'planned_hours': fields.float('Planned Time', digits=(16, 2)),
1307 'user_id': fields.many2one('res.users', 'Responsible'),
1310 'date': fields.date.context_today,
1313 class project_task_history_cumulative(osv.osv):
1314 _name = 'project.task.history.cumulative'
1315 _table = 'project_task_history_cumulative'
1316 _inherit = 'project.task.history'
1320 'end_date': fields.date('End Date'),
1321 'project_id': fields.many2one('project.project', 'Project'),
1325 tools.drop_view_if_exists(cr, 'project_task_history_cumulative')
1327 cr.execute(""" CREATE VIEW project_task_history_cumulative AS (
1329 history.date::varchar||'-'||history.history_id::varchar AS id,
1330 history.date AS end_date,
1335 h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date,
1336 h.task_id, h.type_id, h.user_id, h.kanban_state,
1337 greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours,
1340 project_task_history AS h
1341 JOIN project_task AS t ON (h.task_id = t.id)
1347 class project_category(osv.osv):
1348 """ Category of project's task (or issue) """
1349 _name = "project.category"
1350 _description = "Category of project's task, issue, ..."
1352 'name': fields.char('Name', size=64, required=True, translate=True),
1354 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: