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.'),
47 'block_reason': fields.boolean('Block Reason Required',help='If true, Give the reason for blocking this task'),
50 def _get_default_project_ids(self, cr, uid, ctx={}):
51 project_id = self.pool['project.task']._get_default_project_id(cr, uid, context=ctx)
58 'project_ids': _get_default_project_ids,
63 class project(osv.osv):
64 _name = "project.project"
65 _description = "Project"
66 _inherits = {'account.analytic.account': "analytic_account_id",
67 "mail.alias": "alias_id"}
68 _inherit = ['mail.thread', 'ir.needaction_mixin']
70 def _auto_init(self, cr, context=None):
71 """ Installation hook: aliases, project.project """
72 # create aliases for all projects and avoid constraint errors
73 alias_context = dict(context, alias_model_name='project.task')
74 return self.pool.get('mail.alias').migrate_to_alias(cr, self._name, self._table, super(project, self)._auto_init,
75 'project.task', self._columns['alias_id'], 'id', alias_prefix='project+', alias_defaults={'project_id':'id'}, context=alias_context)
77 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
79 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
80 if context and context.get('user_preference'):
81 cr.execute("""SELECT project.id FROM project_project project
82 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
83 LEFT JOIN project_user_rel rel ON rel.project_id = project.id
84 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
85 return [(r[0]) for r in cr.fetchall()]
86 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
87 context=context, count=count)
89 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
90 partner_obj = self.pool.get('res.partner')
94 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
95 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
96 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
97 val['pricelist_id'] = pricelist_id
100 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
101 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
102 project_ids = [task.project_id.id for task in tasks if task.project_id]
103 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
105 def _get_project_and_parents(self, cr, uid, ids, context=None):
106 """ return the project ids and all their parent projects """
110 SELECT DISTINCT parent.id
111 FROM project_project project, project_project parent, account_analytic_account account
112 WHERE project.analytic_account_id = account.id
113 AND parent.analytic_account_id = account.parent_id
116 ids = [t[0] for t in cr.fetchall()]
120 def _get_project_and_children(self, cr, uid, ids, context=None):
121 """ retrieve all children projects of project ids;
122 return a dictionary mapping each project to its parent project (or None)
124 res = dict.fromkeys(ids, None)
127 SELECT project.id, parent.id
128 FROM project_project project, project_project parent, account_analytic_account account
129 WHERE project.analytic_account_id = account.id
130 AND parent.analytic_account_id = account.parent_id
133 dic = dict(cr.fetchall())
138 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
139 child_parent = self._get_project_and_children(cr, uid, ids, context)
140 # compute planned_hours, total_hours, effective_hours specific to each project
142 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
143 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
145 LEFT JOIN project_task_type ON project_task.stage_id = project_task_type.id
146 WHERE project_task.project_id IN %s AND project_task_type.fold = False
148 """, (tuple(child_parent.keys()),))
149 # aggregate results into res
150 res = dict([(id, {'planned_hours':0.0, 'total_hours':0.0, 'effective_hours':0.0}) for id in ids])
151 for id, planned, total, effective in cr.fetchall():
152 # add the values specific to id to all parent projects of id in the result
155 res[id]['planned_hours'] += planned
156 res[id]['total_hours'] += total
157 res[id]['effective_hours'] += effective
158 id = child_parent[id]
159 # compute progress rates
161 if res[id]['total_hours']:
162 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
164 res[id]['progress_rate'] = 0.0
167 def unlink(self, cr, uid, ids, context=None):
169 mail_alias = self.pool.get('mail.alias')
170 for proj in self.browse(cr, uid, ids, context=context):
172 raise osv.except_osv(_('Invalid Action!'),
173 _('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.'))
175 alias_ids.append(proj.alias_id.id)
176 res = super(project, self).unlink(cr, uid, ids, context=context)
177 mail_alias.unlink(cr, uid, alias_ids, context=context)
180 def _get_attached_docs(self, cr, uid, ids, field_name, arg, context):
182 attachment = self.pool.get('ir.attachment')
183 task = self.pool.get('project.task')
185 project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True)
186 task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context)
187 task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True)
188 res[id] = (project_attachments or 0) + (task_attachments or 0)
191 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
192 """ :deprecated: this method will be removed with OpenERP v8. Use task_ids
196 res = dict.fromkeys(ids, 0)
198 ctx['active_test'] = False
199 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=ctx)
200 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
201 res[task.project_id.id] += 1
204 def _get_alias_models(self, cr, uid, context=None):
205 """ Overriden in project_issue to offer more options """
206 return [('project.task', "Tasks")]
208 def _get_visibility_selection(self, cr, uid, context=None):
209 """ Overriden in portal_project to offer more options """
210 return [('public', 'Public project'),
211 ('employees', 'Internal project: all employees can access'),
212 ('followers', 'Private project: followers Only')]
214 def attachment_tree_view(self, cr, uid, ids, context):
215 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
218 '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),
219 '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)]
220 res_id = ids and ids[0] or False
222 'name': _('Attachments'),
224 'res_model': 'ir.attachment',
225 'type': 'ir.actions.act_window',
227 'view_mode': 'kanban,form',
230 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id)
233 # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
234 _alias_models = lambda self, *args, **kwargs: self._get_alias_models(*args, **kwargs)
235 _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs)
238 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
239 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
240 '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),
241 'priority': fields.integer('Sequence (deprecated)',
242 deprecated='Will be removed with OpenERP v8; use sequence field instead',
243 help="Gives the sequence order when displaying the list of projects"),
244 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members',
245 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)]}),
246 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"),
247 '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.",
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 '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.",
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 '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.",
259 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
260 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20),
262 '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.",
264 'project.project': (_get_project_and_parents, ['tasks', 'parent_id', 'child_ids'], 10),
265 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'stage_id'], 20),
267 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ),
268 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}),
269 'task_count': fields.function(_task_count, type='integer', string="Open Tasks",
270 deprecated="This field will be removed in OpenERP v8. Use task_ids one2many field instead."),
271 'task_ids': fields.one2many('project.task', 'project_id',
272 domain=[('stage_id.fold', '=', False)]),
273 'color': fields.integer('Color Index'),
274 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
275 help="Internal email associated with this project. Incoming emails are automatically synchronized"
276 "with Tasks (or optionally Issues if the Issue Tracker module is installed)."),
277 'alias_model': fields.selection(_alias_models, "Alias Model", select=True, required=True,
278 help="The kind of document created when an email is received on this project's email alias"),
279 'privacy_visibility': fields.selection(_visibility_selection, 'Privacy / Visibility', required=True,
280 help="Holds visibility of the tasks or issues that belong to the current project:\n"
281 "- Public: everybody sees everything; if portal is activated, portal users\n"
282 " see all tasks or issues; if anonymous portal is activated, visitors\n"
283 " see all tasks or issues\n"
284 "- Portal (only available if Portal is installed): employees see everything;\n"
285 " if portal is activated, portal users see the tasks or issues followed by\n"
286 " them or by someone of their company\n"
287 "- Employees Only: employees see all tasks or issues\n"
288 "- Followers Only: employees see only the followed tasks or issues; if portal\n"
289 " is activated, portal users see the followed tasks or issues."),
290 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,),
291 'doc_count': fields.function(
292 _get_attached_docs, string="Number of documents attached", type='integer'
296 def _get_type_common(self, cr, uid, context):
297 ids = self.pool.get('project.task.type').search(cr, uid, [('case_default','=',1)], context=context)
300 _order = "sequence, id"
306 'type_ids': _get_type_common,
307 'alias_model': 'project.task',
308 'privacy_visibility': 'employees',
311 # TODO: Why not using a SQL contraints ?
312 def _check_dates(self, cr, uid, ids, context=None):
313 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
314 if leave['date_start'] and leave['date']:
315 if leave['date_start'] > leave['date']:
320 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
323 def set_template(self, cr, uid, ids, context=None):
324 return self.setActive(cr, uid, ids, value=False, context=context)
326 def set_done(self, cr, uid, ids, context=None):
327 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
329 def set_cancel(self, cr, uid, ids, context=None):
330 return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
332 def set_pending(self, cr, uid, ids, context=None):
333 return self.write(cr, uid, ids, {'state': 'pending'}, context=context)
335 def set_open(self, cr, uid, ids, context=None):
336 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
338 def reset_project(self, cr, uid, ids, context=None):
339 return self.setActive(cr, uid, ids, value=True, context=context)
341 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
342 """ copy and map tasks from old to new project """
346 task_obj = self.pool.get('project.task')
347 proj = self.browse(cr, uid, old_project_id, context=context)
348 for task in proj.tasks:
349 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
350 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
351 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
354 def copy(self, cr, uid, id, default=None, context=None):
360 context['active_test'] = False
361 default['state'] = 'open'
362 default['line_ids'] = []
363 default['tasks'] = []
365 # Don't prepare (expensive) data to copy children (analytic accounts),
366 # they are discarded in analytic.copy(), and handled in duplicate_template()
367 default['child_ids'] = []
369 proj = self.browse(cr, uid, id, context=context)
370 if not default.get('name', False):
371 default.update(name=_("%s (copy)") % (proj.name))
372 res = super(project, self).copy(cr, uid, id, default, context)
373 self.map_tasks(cr, uid, id, res, context=context)
376 def duplicate_template(self, cr, uid, ids, context=None):
379 data_obj = self.pool.get('ir.model.data')
381 for proj in self.browse(cr, uid, ids, context=context):
382 parent_id = context.get('parent_id', False)
383 context.update({'analytic_project_copy': True})
384 new_date_start = time.strftime('%Y-%m-%d')
386 if proj.date_start and proj.date:
387 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
388 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
389 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
390 context.update({'copy':True})
391 new_id = self.copy(cr, uid, proj.id, default = {
392 'name':_("%s (copy)") % (proj.name),
394 'date_start':new_date_start,
396 'parent_id':parent_id}, context=context)
397 result.append(new_id)
399 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
400 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
402 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
404 if result and len(result):
406 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
407 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
408 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
409 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
410 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
411 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
413 'name': _('Projects'),
415 'view_mode': 'form,tree',
416 'res_model': 'project.project',
419 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
420 'type': 'ir.actions.act_window',
421 'search_view_id': search_view['res_id'],
425 # set active value for a project, its sub projects and its tasks
426 def setActive(self, cr, uid, ids, value=True, context=None):
427 task_obj = self.pool.get('project.task')
428 for proj in self.browse(cr, uid, ids, context=None):
429 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
430 cr.execute('select id from project_task where project_id=%s', (proj.id,))
431 tasks_id = [x[0] for x in cr.fetchall()]
433 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
434 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
436 self.setActive(cr, uid, child_ids, value, context=None)
439 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
440 context = context or {}
441 if type(ids) in (long, int,):
443 projects = self.browse(cr, uid, ids, context=context)
445 for project in projects:
446 if (not project.members) and force_members:
447 raise osv.except_osv(_('Warning!'),_("You must assign members on the project '%s'!") % (project.name,))
449 resource_pool = self.pool.get('resource.resource')
451 result = "from openerp.addons.resource.faces import *\n"
452 result += "import datetime\n"
453 for project in self.browse(cr, uid, ids, context=context):
454 u_ids = [i.id for i in project.members]
455 if project.user_id and (project.user_id.id not in u_ids):
456 u_ids.append(project.user_id.id)
457 for task in project.tasks:
458 if task.user_id and (task.user_id.id not in u_ids):
459 u_ids.append(task.user_id.id)
460 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
461 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
462 for key, vals in resource_objs.items():
464 class User_%s(Resource):
466 ''' % (key, vals.get('efficiency', False))
473 def _schedule_project(self, cr, uid, project, context=None):
474 resource_pool = self.pool.get('resource.resource')
475 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
476 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
477 # TODO: check if we need working_..., default values are ok.
478 puids = [x.id for x in project.members]
480 puids.append(project.user_id.id)
488 project.date_start or time.strftime('%Y-%m-%d'), working_days,
489 '|'.join(['User_'+str(x) for x in puids]) or 'None'
491 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
498 #TODO: DO Resource allocation and compute availability
499 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
505 def schedule_tasks(self, cr, uid, ids, context=None):
506 context = context or {}
507 if type(ids) in (long, int,):
509 projects = self.browse(cr, uid, ids, context=context)
510 result = self._schedule_header(cr, uid, ids, False, context=context)
511 for project in projects:
512 result += self._schedule_project(cr, uid, project, context=context)
513 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
516 exec result in local_dict
517 projects_gantt = Task.BalancedProject(local_dict['Project'])
519 for project in projects:
520 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
521 for task in project.tasks:
522 if task.stage_id and task.stage_id.fold:
525 p = getattr(project_gantt, 'Task_%d' % (task.id,))
527 self.pool.get('project.task').write(cr, uid, [task.id], {
528 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
529 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
531 if (not task.user_id) and (p.booked_resource):
532 self.pool.get('project.task').write(cr, uid, [task.id], {
533 'user_id': int(p.booked_resource[0].name[5:]),
537 def create(self, cr, uid, vals, context=None):
540 # Prevent double project creation when 'use_tasks' is checked + alias management
541 create_context = dict(context, project_creation_in_progress=True,
542 alias_model_name=vals.get('alias_model', 'project.task'),
543 alias_parent_model_name=self._name)
545 if vals.get('type', False) not in ('template', 'contract'):
546 vals['type'] = 'contract'
548 project_id = super(project, self).create(cr, uid, vals, context=create_context)
549 project_rec = self.browse(cr, uid, project_id, context=context)
550 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)
553 def write(self, cr, uid, ids, vals, context=None):
554 # if alias_model has been changed, update alias_model_id accordingly
555 if vals.get('alias_model'):
556 model_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', vals.get('alias_model', 'project.task'))])
557 vals.update(alias_model_id=model_ids[0])
558 return super(project, self).write(cr, uid, ids, vals, context=context)
562 _name = "project.task"
563 _description = "Task"
564 _date_name = "date_start"
565 _inherit = ['mail.thread', 'ir.needaction_mixin']
567 _mail_post_access = 'read'
570 # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
571 'project.mt_task_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence <= 1,
572 'project.mt_task_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id.sequence > 1,
575 'project.mt_task_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id,
578 'project.mt_task_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked',
579 'project.mt_task_done': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'done',
583 def _get_default_partner(self, cr, uid, context=None):
584 project_id = self._get_default_project_id(cr, uid, context)
586 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
587 if project and project.partner_id:
588 return project.partner_id.id
591 def _get_default_project_id(self, cr, uid, context=None):
592 """ Gives default section by checking if present in the context """
593 return (self._resolve_project_id_from_context(cr, uid, context=context) or False)
595 def _get_default_stage_id(self, cr, uid, context=None):
596 """ Gives default stage_id """
597 project_id = self._get_default_project_id(cr, uid, context=context)
598 return self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context)
600 def _resolve_project_id_from_context(self, cr, uid, context=None):
601 """ Returns ID of project based on the value of 'default_project_id'
602 context key, or None if it cannot be resolved to a single
607 if type(context.get('default_project_id')) in (int, long):
608 return context['default_project_id']
609 if isinstance(context.get('default_project_id'), basestring):
610 project_name = context['default_project_id']
611 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
612 if len(project_ids) == 1:
613 return project_ids[0][0]
616 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
617 stage_obj = self.pool.get('project.task.type')
618 order = stage_obj._order
619 access_rights_uid = access_rights_uid or uid
620 if read_group_order == 'stage_id desc':
621 order = '%s desc' % order
623 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
625 search_domain += ['|', ('project_ids', '=', project_id)]
626 search_domain += [('id', 'in', ids)]
627 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
628 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
629 # restore order of the search
630 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
633 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
634 fold[stage.id] = stage.fold or False
637 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
638 res_users = self.pool.get('res.users')
639 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
640 access_rights_uid = access_rights_uid or uid
642 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
643 order = res_users._order
644 # lame way to allow reverting search, should just work in the trivial case
645 if read_group_order == 'user_id desc':
646 order = '%s desc' % order
647 # de-duplicate and apply search order
648 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
649 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
650 # restore order of the search
651 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
655 'stage_id': _read_group_stage_ids,
656 'user_id': _read_group_user_id,
659 def _str_get(self, task, level=0, border='***', context=None):
660 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'+ \
661 border[0]+' '+(task.name or '')+'\n'+ \
662 (task.description or '')+'\n\n'
664 # Compute: effective_hours, total_hours, progress
665 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
667 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
668 hours = dict(cr.fetchall())
669 for task in self.browse(cr, uid, ids, context=context):
670 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)}
671 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
672 res[task.id]['progress'] = 0.0
673 if (task.remaining_hours + hours.get(task.id, 0.0)):
674 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
675 # TDE CHECK: if task.state in ('done','cancelled'):
676 if task.stage_id and task.stage_id.fold:
677 res[task.id]['progress'] = 100.0
680 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned=0.0):
681 if remaining and not planned:
682 return {'value': {'planned_hours': remaining}}
685 def onchange_planned(self, cr, uid, ids, planned=0.0, effective=0.0):
686 return {'value': {'remaining_hours': planned - effective}}
688 def onchange_project(self, cr, uid, id, project_id, context=None):
690 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
691 if project and project.partner_id:
692 return {'value': {'partner_id': project.partner_id.id}}
695 def onchange_user_id(self, cr, uid, ids, user_id, context=None):
698 vals['date_start'] = fields.datetime.now()
699 return {'value': vals}
701 def duplicate_task(self, cr, uid, map_ids, context=None):
702 mapper = lambda t: map_ids.get(t.id, t.id)
703 for task in self.browse(cr, uid, map_ids.values(), context):
704 new_child_ids = set(map(mapper, task.child_ids))
705 new_parent_ids = set(map(mapper, task.parent_ids))
706 if new_child_ids or new_parent_ids:
707 task.write({'parent_ids': [(6,0,list(new_parent_ids))],
708 'child_ids': [(6,0,list(new_child_ids))]})
710 def copy_data(self, cr, uid, id, default=None, context=None):
713 default = default or {}
714 default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False})
715 if not default.get('remaining_hours', False):
716 default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours'])
717 default['active'] = True
718 if not default.get('name', False):
719 default['name'] = self.browse(cr, uid, id, context=context).name or ''
720 if not context.get('copy',False):
721 new_name = _("%s (copy)") % (default.get('name', ''))
722 default.update({'name':new_name})
723 return super(task, self).copy_data(cr, uid, id, default, context)
725 def copy(self, cr, uid, id, default=None, context=None):
730 if not context.get('copy', False):
731 stage = self._get_default_stage_id(cr, uid, context=context)
733 default['stage_id'] = stage
734 return super(task, self).copy(cr, uid, id, default, context)
736 def _is_template(self, cr, uid, ids, field_name, arg, context=None):
738 for task in self.browse(cr, uid, ids, context=context):
741 if task.project_id.active == False or task.project_id.state == 'template':
745 def _get_task(self, cr, uid, ids, context=None):
747 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
748 if work.task_id: result[work.task_id.id] = True
752 '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."),
753 'name': fields.char('Task Summary', track_visibility='onchange', size=128, required=True, select=True),
754 'description': fields.text('Description'),
755 'priority': fields.selection([('0','Low'), ('1','Normal'), ('2','High')], 'Priority', select=True),
756 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
757 'stage_id': fields.many2one('project.task.type', 'Stage', track_visibility='onchange', select=True,
758 domain="[('project_ids', '=', project_id)]"),
759 'categ_ids': fields.many2many('project.category', string='Tags'),
760 'kanban_state': fields.selection([('normal', 'In Progress'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
761 track_visibility='onchange',
762 help="A task's kanban state indicates special situations affecting it:\n"
763 " * Normal is the default situation\n"
764 " * Blocked indicates something is preventing the progress of this task\n"
765 " * Ready for next stage indicates the task is ready to be pulled to the next stage",
767 'create_date': fields.datetime('Create Date', readonly=True, select=True),
768 '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)
769 'date_start': fields.datetime('Starting Date',select=True),
770 'date_end': fields.datetime('Ending Date',select=True),
771 'date_deadline': fields.date('Deadline',select=True),
772 'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
773 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select=True, track_visibility='onchange', change_default=True),
774 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
775 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
776 'notes': fields.text('Notes'),
777 '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.'),
778 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
780 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
781 'project.task.work': (_get_task, ['hours'], 10),
783 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
784 'total_hours': fields.function(_hours_get, string='Total', multi='hours', help="Computed as: Time Spent + Remaining Time.",
786 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
787 'project.task.work': (_get_task, ['hours'], 10),
789 '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",
791 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10),
792 'project.task.work': (_get_task, ['hours'], 10),
794 '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.",
796 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
797 'project.task.work': (_get_task, ['hours'], 10),
799 'user_id': fields.many2one('res.users', 'Assigned to', select=True, track_visibility='onchange'),
800 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
801 'partner_id': fields.many2one('res.partner', 'Customer'),
802 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
803 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
804 'company_id': fields.many2one('res.company', 'Company'),
805 'id': fields.integer('ID', readonly=True),
806 'color': fields.integer('Color Index'),
807 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
810 'stage_id': _get_default_stage_id,
811 'project_id': _get_default_project_id,
812 'date_last_stage_update': fields.datetime.now,
813 'kanban_state': 'normal',
818 'user_id': lambda obj, cr, uid, ctx=None: uid,
819 'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=ctx),
820 'partner_id': lambda self, cr, uid, ctx=None: self._get_default_partner(cr, uid, context=ctx),
822 _order = "priority, sequence, date_start, name, id"
824 def _check_recursion(self, cr, uid, ids, context=None):
826 visited_branch = set()
828 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
834 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
835 if id in visited_branch: #Cycle
838 if id in visited_node: #Already tested don't work one more time for nothing
841 visited_branch.add(id)
844 #visit child using DFS
845 task = self.browse(cr, uid, id, context=context)
846 for child in task.child_ids:
847 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
851 visited_branch.remove(id)
854 def _check_dates(self, cr, uid, ids, context=None):
857 obj_task = self.browse(cr, uid, ids[0], context=context)
858 start = obj_task.date_start or False
859 end = obj_task.date_end or False
866 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
867 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
870 # Override view according to the company definition
871 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
872 users_obj = self.pool.get('res.users')
873 if context is None: context = {}
874 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
875 # this should be safe (no context passed to avoid side-effects)
876 obj_tm = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id
877 tm = obj_tm and obj_tm.name or 'Hours'
879 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
881 if tm in ['Hours','Hour']:
884 eview = etree.fromstring(res['arch'])
886 def _check_rec(eview):
887 if eview.attrib.get('widget','') == 'float_time':
888 eview.set('widget','float')
895 res['arch'] = etree.tostring(eview)
897 for f in res['fields']:
898 if 'Hours' in res['fields'][f]['string']:
899 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
902 def get_empty_list_help(self, cr, uid, help, context=None):
903 context['empty_list_help_id'] = context.get('default_project_id')
904 context['empty_list_help_model'] = 'project.project'
905 context['empty_list_help_document_name'] = _("tasks")
906 return super(task, self).get_empty_list_help(cr, uid, help, context=context)
908 # ----------------------------------------
910 # ----------------------------------------
912 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
913 """ Override of the base.stage method
914 Parameter of the stage search taken from the lead:
915 - section_id: if set, stages must belong to this section or
916 be a default stage; if not set, stages must be default
919 if isinstance(cases, (int, long)):
920 cases = self.browse(cr, uid, cases, context=context)
921 # collect all section_ids
924 section_ids.append(section_id)
927 section_ids.append(task.project_id.id)
930 search_domain = [('|')] * (len(section_ids) - 1)
931 for section_id in section_ids:
932 search_domain.append(('project_ids', '=', section_id))
933 search_domain += list(domain)
934 # perform search, return the first found
935 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
940 def _check_child_task(self, cr, uid, ids, context=None):
943 tasks = self.browse(cr, uid, ids, context=context)
946 for child in task.child_ids:
947 if child.stage_id and not child.stage_id.fold:
948 raise osv.except_osv(_("Warning!"), _("Child task still open.\nPlease cancel or complete child task first."))
951 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
952 attachment = self.pool.get('ir.attachment')
953 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
954 new_attachment_ids = []
955 for attachment_id in attachment_ids:
956 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
957 return new_attachment_ids
959 def do_delegate(self, cr, uid, ids, delegate_data=None, context=None):
961 Delegate Task to another users.
963 if delegate_data is None:
965 assert delegate_data['user_id'], _("Delegated User should be specified")
967 for task in self.browse(cr, uid, ids, context=context):
968 delegated_task_id = self.copy(cr, uid, task.id, {
969 'name': delegate_data['name'],
970 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
971 'stage_id': delegate_data.get('stage_id') and delegate_data.get('stage_id')[0] or False,
972 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
973 'planned_hours': delegate_data['planned_hours'] or 0.0,
974 'parent_ids': [(6, 0, [task.id])],
975 'description': delegate_data['new_task_description'] or '',
979 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
980 newname = delegate_data['prefix'] or ''
982 'remaining_hours': delegate_data['planned_hours_me'],
983 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
986 delegated_tasks[task.id] = delegated_task_id
987 return delegated_tasks
989 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
990 for task in self.browse(cr, uid, ids, context=context):
991 if (task.stage_id and task.stage_id.sequence <= 1) or (task.planned_hours == 0.0):
992 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
993 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
996 def set_remaining_time_1(self, cr, uid, ids, context=None):
997 return self.set_remaining_time(cr, uid, ids, 1.0, context)
999 def set_remaining_time_2(self, cr, uid, ids, context=None):
1000 return self.set_remaining_time(cr, uid, ids, 2.0, context)
1002 def set_remaining_time_5(self, cr, uid, ids, context=None):
1003 return self.set_remaining_time(cr, uid, ids, 5.0, context)
1005 def set_remaining_time_10(self, cr, uid, ids, context=None):
1006 return self.set_remaining_time(cr, uid, ids, 10.0, context)
1008 def _store_history(self, cr, uid, ids, context=None):
1009 for task in self.browse(cr, uid, ids, context=context):
1010 self.pool.get('project.task.history').create(cr, uid, {
1012 'remaining_hours': task.remaining_hours,
1013 'planned_hours': task.planned_hours,
1014 'kanban_state': task.kanban_state,
1015 'type_id': task.stage_id.id,
1016 'user_id': task.user_id.id
1021 # ------------------------------------------------
1023 # ------------------------------------------------
1025 def create(self, cr, uid, vals, context=None):
1030 if vals.get('project_id') and not context.get('default_project_id'):
1031 context['default_project_id'] = vals.get('project_id')
1032 # user_id change: update date_start
1033 if vals.get('user_id') and not vals.get('start_date'):
1034 vals['date_start'] = fields.datetime.now()
1036 # context: no_log, because subtype already handle this
1037 create_context = dict(context, mail_create_nolog=True)
1038 task_id = super(task, self).create(cr, uid, vals, context=create_context)
1039 self._store_history(cr, uid, [task_id], context=context)
1042 def write(self, cr, uid, ids, vals, context=None):
1043 if isinstance(ids, (int, long)):
1046 # stage change: update date_last_stage_update
1047 if 'stage_id' in vals:
1048 vals['date_last_stage_update'] = fields.datetime.now()
1049 # user_id change: update date_start
1050 if vals.get('user_id') and 'date_start' not in vals:
1051 vals['date_start'] = fields.datetime.now()
1053 # Overridden to reset the kanban_state to normal whenever
1054 # the stage (stage_id) of the task changes.
1055 if vals and not 'kanban_state' in vals and 'stage_id' in vals:
1056 new_stage = vals.get('stage_id')
1057 vals_reset_kstate = dict(vals, kanban_state='normal')
1058 for t in self.browse(cr, uid, ids, context=context):
1059 write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
1060 super(task, self).write(cr, uid, [t.id], write_vals, context=context)
1063 result = super(task, self).write(cr, uid, ids, vals, context=context)
1065 if any(item in vals for item in ['stage_id', 'remaining_hours', 'user_id', 'kanban_state']):
1066 self._store_history(cr, uid, ids, context=context)
1069 def unlink(self, cr, uid, ids, context=None):
1072 self._check_child_task(cr, uid, ids, context=context)
1073 res = super(task, self).unlink(cr, uid, ids, context)
1076 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1077 context = context or {}
1081 if task.stage_id and task.stage_id.fold:
1086 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1088 for t2 in task.parent_ids:
1089 start.append("up.Task_%s.end" % (t2.id,))
1093 ''' % (ident,','.join(start))
1098 ''' % (ident, 'User_'+str(task.user_id.id))
1103 # ---------------------------------------------------
1105 # ---------------------------------------------------
1107 def message_get_reply_to(self, cr, uid, ids, context=None):
1108 """ Override to get the reply_to of the parent project. """
1109 return [task.project_id.message_get_reply_to()[0] if task.project_id else False
1110 for task in self.browse(cr, uid, ids, context=context)]
1112 def message_new(self, cr, uid, msg, custom_values=None, context=None):
1113 """ Override to updates the document according to the email. """
1114 if custom_values is None:
1117 'name': msg.get('subject'),
1118 'planned_hours': 0.0,
1120 defaults.update(custom_values)
1121 return super(task, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
1123 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
1124 """ Override to update the task according to the email. """
1125 if update_vals is None:
1128 'cost': 'planned_hours',
1130 for line in msg['body'].split('\n'):
1132 res = tools.command_re.match(line)
1134 match = res.group(1).lower()
1135 field = maps.get(match)
1138 update_vals[field] = float(res.group(2).lower())
1139 except (ValueError, TypeError):
1141 return super(task, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
1143 class project_work(osv.osv):
1144 _name = "project.task.work"
1145 _description = "Project Task Work"
1147 'name': fields.char('Work summary', size=128),
1148 'date': fields.datetime('Date', select="1"),
1149 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1150 'hours': fields.float('Time Spent'),
1151 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1152 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1156 'user_id': lambda obj, cr, uid, context: uid,
1157 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1160 _order = "date desc"
1161 def create(self, cr, uid, vals, *args, **kwargs):
1162 if 'hours' in vals and (not vals['hours']):
1163 vals['hours'] = 0.00
1164 if 'task_id' in vals:
1165 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1166 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1168 def write(self, cr, uid, ids, vals, context=None):
1169 if 'hours' in vals and (not vals['hours']):
1170 vals['hours'] = 0.00
1172 for work in self.browse(cr, uid, ids, context=context):
1173 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))
1174 return super(project_work,self).write(cr, uid, ids, vals, context)
1176 def unlink(self, cr, uid, ids, *args, **kwargs):
1177 for work in self.browse(cr, uid, ids):
1178 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1179 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1182 class account_analytic_account(osv.osv):
1183 _inherit = 'account.analytic.account'
1184 _description = 'Analytic Account'
1186 '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"),
1187 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
1190 def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
1191 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
1192 if template_id and 'value' in res:
1193 template = self.browse(cr, uid, template_id, context=context)
1194 res['value']['use_tasks'] = template.use_tasks
1197 def _trigger_project_creation(self, cr, uid, vals, context=None):
1199 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.
1201 if context is None: context = {}
1202 return vals.get('use_tasks') and not 'project_creation_in_progress' in context
1204 def project_create(self, cr, uid, analytic_account_id, vals, context=None):
1206 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.
1208 project_pool = self.pool.get('project.project')
1209 project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)])
1210 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):
1212 'name': vals.get('name'),
1213 'analytic_account_id': analytic_account_id,
1214 'type': vals.get('type','contract'),
1216 return project_pool.create(cr, uid, project_values, context=context)
1219 def create(self, cr, uid, vals, context=None):
1222 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1223 vals['child_ids'] = []
1224 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
1225 self.project_create(cr, uid, analytic_account_id, vals, context=context)
1226 return analytic_account_id
1228 def write(self, cr, uid, ids, vals, context=None):
1229 if isinstance(ids, (int, long)):
1231 vals_for_project = vals.copy()
1232 for account in self.browse(cr, uid, ids, context=context):
1233 if not vals.get('name'):
1234 vals_for_project['name'] = account.name
1235 if not vals.get('type'):
1236 vals_for_project['type'] = account.type
1237 self.project_create(cr, uid, account.id, vals_for_project, context=context)
1238 return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context)
1240 def unlink(self, cr, uid, ids, *args, **kwargs):
1241 project_obj = self.pool.get('project.project')
1242 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1244 raise osv.except_osv(_('Warning!'), _('Please delete the project linked with this account first.'))
1245 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1247 def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
1252 if context.get('current_model') == 'project.project':
1253 project_ids = self.search(cr, uid, args + [('name', operator, name)], limit=limit, context=context)
1254 return self.name_get(cr, uid, project_ids, context=context)
1256 return super(account_analytic_account, self).name_search(cr, uid, name, args=args, operator=operator, context=context, limit=limit)
1259 class project_project(osv.osv):
1260 _inherit = 'project.project'
1265 class project_task_history(osv.osv):
1267 Tasks History, used for cumulative flow charts (Lean/Agile)
1269 _name = 'project.task.history'
1270 _description = 'History of Tasks'
1271 _rec_name = 'task_id'
1274 def _get_date(self, cr, uid, ids, name, arg, context=None):
1276 for history in self.browse(cr, uid, ids, context=context):
1277 if history.type_id and history.type_id.fold:
1278 result[history.id] = history.date
1280 cr.execute('''select
1283 project_task_history
1287 order by id limit 1''', (history.task_id.id, history.id))
1289 result[history.id] = res and res[0] or False
1292 def _get_related_date(self, cr, uid, ids, context=None):
1294 for history in self.browse(cr, uid, ids, context=context):
1295 cr.execute('''select
1298 project_task_history
1302 order by id desc limit 1''', (history.task_id.id, history.id))
1305 result.append(res[0])
1309 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1310 'type_id': fields.many2one('project.task.type', 'Stage'),
1311 'kanban_state': fields.selection([('normal', 'Normal'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], 'Kanban State', required=False),
1312 'date': fields.date('Date', select=True),
1313 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1314 'project.task.history': (_get_related_date, None, 20)
1316 'remaining_hours': fields.float('Remaining Time', digits=(16, 2)),
1317 'planned_hours': fields.float('Planned Time', digits=(16, 2)),
1318 'user_id': fields.many2one('res.users', 'Responsible'),
1321 'date': fields.date.context_today,
1324 class project_task_history_cumulative(osv.osv):
1325 _name = 'project.task.history.cumulative'
1326 _table = 'project_task_history_cumulative'
1327 _inherit = 'project.task.history'
1331 'end_date': fields.date('End Date'),
1332 'project_id': fields.many2one('project.project', 'Project'),
1336 tools.drop_view_if_exists(cr, 'project_task_history_cumulative')
1338 cr.execute(""" CREATE VIEW project_task_history_cumulative AS (
1340 history.date::varchar||'-'||history.history_id::varchar AS id,
1341 history.date AS end_date,
1346 h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date,
1347 h.task_id, h.type_id, h.user_id, h.kanban_state,
1348 greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours,
1351 project_task_history AS h
1352 JOIN project_task AS t ON (h.task_id = t.id)
1358 class project_category(osv.osv):
1359 """ Category of project's task (or issue) """
1360 _name = "project.category"
1361 _description = "Category of project's task, issue, ..."
1363 'name': fields.char('Name', size=64, required=True, translate=True),
1365 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: