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 by Default',
45 help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
51 'case_default': False,
52 'project_ids': lambda self, cr, uid, ctx=None: self.pool['project.task']._get_default_project_id(cr, uid, context=ctx),
57 class project(osv.osv):
58 _name = "project.project"
59 _description = "Project"
60 _inherits = {'account.analytic.account': "analytic_account_id",
61 "mail.alias": "alias_id"}
62 _inherit = ['mail.thread', 'ir.needaction_mixin']
64 def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
66 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count)
67 if context and context.get('user_preference'):
68 cr.execute("""SELECT project.id FROM project_project project
69 LEFT JOIN account_analytic_account account ON account.id = project.analytic_account_id
70 LEFT JOIN project_user_rel rel ON rel.project_id = project.id
71 WHERE (account.user_id = %s or rel.uid = %s)"""%(user, user))
72 return [(r[0]) for r in cr.fetchall()]
73 return super(project, self).search(cr, user, args, offset=offset, limit=limit, order=order,
74 context=context, count=count)
76 def _complete_name(self, cr, uid, ids, name, args, context=None):
78 for m in self.browse(cr, uid, ids, context=context):
79 res[m.id] = (m.parent_id and (m.parent_id.name + '/') or '') + m.name
82 def onchange_partner_id(self, cr, uid, ids, part=False, context=None):
83 partner_obj = self.pool.get('res.partner')
87 if 'pricelist_id' in self.fields_get(cr, uid, context=context):
88 pricelist = partner_obj.read(cr, uid, part, ['property_product_pricelist'], context=context)
89 pricelist_id = pricelist.get('property_product_pricelist', False) and pricelist.get('property_product_pricelist')[0] or False
90 val['pricelist_id'] = pricelist_id
93 def _get_projects_from_tasks(self, cr, uid, task_ids, context=None):
94 tasks = self.pool.get('project.task').browse(cr, uid, task_ids, context=context)
95 project_ids = [task.project_id.id for task in tasks if task.project_id]
96 return self.pool.get('project.project')._get_project_and_parents(cr, uid, project_ids, context)
98 def _get_project_and_parents(self, cr, uid, ids, context=None):
99 """ return the project ids and all their parent projects """
103 SELECT DISTINCT parent.id
104 FROM project_project project, project_project parent, account_analytic_account account
105 WHERE project.analytic_account_id = account.id
106 AND parent.analytic_account_id = account.parent_id
109 ids = [t[0] for t in cr.fetchall()]
113 def _get_project_and_children(self, cr, uid, ids, context=None):
114 """ retrieve all children projects of project ids;
115 return a dictionary mapping each project to its parent project (or None)
117 res = dict.fromkeys(ids, None)
120 SELECT project.id, parent.id
121 FROM project_project project, project_project parent, account_analytic_account account
122 WHERE project.analytic_account_id = account.id
123 AND parent.analytic_account_id = account.parent_id
126 dic = dict(cr.fetchall())
131 def _progress_rate(self, cr, uid, ids, names, arg, context=None):
132 child_parent = self._get_project_and_children(cr, uid, ids, context)
133 # compute planned_hours, total_hours, effective_hours specific to each project
135 SELECT project_id, COALESCE(SUM(planned_hours), 0.0),
136 COALESCE(SUM(total_hours), 0.0), COALESCE(SUM(effective_hours), 0.0)
138 LEFT JOIN project_task_type ON project_task.stage_id = project_task_type.id
139 WHERE project_task.project_id IN %s AND project_task_type.fold = False
141 """, (tuple(child_parent.keys()),))
142 # aggregate results into res
143 res = dict([(id, {'planned_hours':0.0, 'total_hours':0.0, 'effective_hours':0.0}) for id in ids])
144 for id, planned, total, effective in cr.fetchall():
145 # add the values specific to id to all parent projects of id in the result
148 res[id]['planned_hours'] += planned
149 res[id]['total_hours'] += total
150 res[id]['effective_hours'] += effective
151 id = child_parent[id]
152 # compute progress rates
154 if res[id]['total_hours']:
155 res[id]['progress_rate'] = round(100.0 * res[id]['effective_hours'] / res[id]['total_hours'], 2)
157 res[id]['progress_rate'] = 0.0
160 def unlink(self, cr, uid, ids, context=None):
162 mail_alias = self.pool.get('mail.alias')
163 for proj in self.browse(cr, uid, ids, context=context):
165 raise osv.except_osv(_('Invalid Action!'),
166 _('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.'))
168 alias_ids.append(proj.alias_id.id)
169 res = super(project, self).unlink(cr, uid, ids, context=context)
170 mail_alias.unlink(cr, uid, alias_ids, context=context)
173 def _get_attached_docs(self, cr, uid, ids, field_name, arg, context):
175 attachment = self.pool.get('ir.attachment')
176 task = self.pool.get('project.task')
178 project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', id)], context=context, count=True)
179 task_ids = task.search(cr, uid, [('project_id', '=', id)], context=context)
180 task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True)
181 res[id] = (project_attachments or 0) + (task_attachments or 0)
184 def _task_count(self, cr, uid, ids, field_name, arg, context=None):
187 res = dict.fromkeys(ids, 0)
189 ctx['active_test'] = False
190 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)], context=ctx)
191 for task in self.pool.get('project.task').browse(cr, uid, task_ids, context):
192 res[task.project_id.id] += 1
195 def _get_alias_models(self, cr, uid, context=None):
196 """ Overriden in project_issue to offer more options """
197 return [('project.task', "Tasks")]
199 def _get_visibility_selection(self, cr, uid, context=None):
200 """ Overriden in portal_project to offer more options """
201 return [('public', 'Public project'),
202 ('employees', 'Internal project: all employees can access'),
203 ('followers', 'Private project: followers Only')]
205 def attachment_tree_view(self, cr, uid, ids, context):
206 task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)])
209 '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids),
210 '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)]
211 res_id = ids and ids[0] or False
213 'name': _('Attachments'),
215 'res_model': 'ir.attachment',
216 'type': 'ir.actions.act_window',
218 'view_mode': 'tree,form',
221 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id)
224 # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
225 _alias_models = lambda self, *args, **kwargs: self._get_alias_models(*args, **kwargs)
226 _visibility_selection = lambda self, *args, **kwargs: self._get_visibility_selection(*args, **kwargs)
229 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250),
230 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."),
231 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."),
232 '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),
233 'priority': fields.integer('Sequence', 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="Open Tasks"),
260 'color': fields.integer('Color Index'),
261 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
262 help="Internal email associated with this project. Incoming emails are automatically synchronized"
263 "with Tasks (or optionally Issues if the Issue Tracker module is installed)."),
264 'alias_model': fields.selection(_alias_models, "Alias Model", select=True, required=True,
265 help="The kind of document created when an email is received on this project's email alias"),
266 'privacy_visibility': fields.selection(_visibility_selection, 'Privacy / Visibility', required=True,
267 help="Holds visibility of the tasks or issues that belong to the current project:\n"
268 "- Public: everybody sees everything; if portal is activated, portal users\n"
269 " see all tasks or issues; if anonymous portal is activated, visitors\n"
270 " see all tasks or issues\n"
271 "- Portal (only available if Portal is installed): employees see everything;\n"
272 " if portal is activated, portal users see the tasks or issues followed by\n"
273 " them or by someone of their company\n"
274 "- Employees Only: employees see all tasks or issues\n"
275 "- Followers Only: employees see only the followed tasks or issues; if portal\n"
276 " is activated, portal users see the followed tasks or issues."),
277 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,),
278 'doc_count':fields.function(_get_attached_docs, string="Number of documents attached", type='int')
281 def _get_type_common(self, cr, uid, context):
282 ids = self.pool.get('project.task.type').search(cr, uid, [('case_default','=',1)], context=context)
285 _order = "sequence, id"
292 'type_ids': _get_type_common,
293 'alias_model': 'project.task',
294 'privacy_visibility': 'employees',
297 # TODO: Why not using a SQL contraints ?
298 def _check_dates(self, cr, uid, ids, context=None):
299 for leave in self.read(cr, uid, ids, ['date_start', 'date'], context=context):
300 if leave['date_start'] and leave['date']:
301 if leave['date_start'] > leave['date']:
306 (_check_dates, 'Error! project start-date must be lower then project end-date.', ['date_start', 'date'])
309 def set_template(self, cr, uid, ids, context=None):
310 return self.setActive(cr, uid, ids, value=False, context=context)
312 def set_done(self, cr, uid, ids, context=None):
313 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
315 def set_cancel(self, cr, uid, ids, context=None):
316 return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
318 def set_pending(self, cr, uid, ids, context=None):
319 return self.write(cr, uid, ids, {'state': 'pending'}, context=context)
321 def set_open(self, cr, uid, ids, context=None):
322 return self.write(cr, uid, ids, {'state': 'open'}, context=context)
324 def reset_project(self, cr, uid, ids, context=None):
325 return self.setActive(cr, uid, ids, value=True, context=context)
327 def map_tasks(self, cr, uid, old_project_id, new_project_id, context=None):
328 """ copy and map tasks from old to new project """
332 task_obj = self.pool.get('project.task')
333 proj = self.browse(cr, uid, old_project_id, context=context)
334 for task in proj.tasks:
335 map_task_id[task.id] = task_obj.copy(cr, uid, task.id, {}, context=context)
336 self.write(cr, uid, [new_project_id], {'tasks':[(6,0, map_task_id.values())]})
337 task_obj.duplicate_task(cr, uid, map_task_id, context=context)
340 def copy(self, cr, uid, id, default=None, context=None):
346 context['active_test'] = False
347 default['state'] = 'open'
348 default['line_ids'] = []
349 default['tasks'] = []
350 proj = self.browse(cr, uid, id, context=context)
351 if not default.get('name', False):
352 default.update(name=_("%s (copy)") % (proj.name))
353 res = super(project, self).copy(cr, uid, id, default, context)
354 self.map_tasks(cr, uid, id, res, context=context)
357 def duplicate_template(self, cr, uid, ids, context=None):
360 data_obj = self.pool.get('ir.model.data')
362 for proj in self.browse(cr, uid, ids, context=context):
363 parent_id = context.get('parent_id', False)
364 context.update({'analytic_project_copy': True})
365 new_date_start = time.strftime('%Y-%m-%d')
367 if proj.date_start and proj.date:
368 start_date = date(*time.strptime(proj.date_start,'%Y-%m-%d')[:3])
369 end_date = date(*time.strptime(proj.date,'%Y-%m-%d')[:3])
370 new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d')
371 context.update({'copy':True})
372 new_id = self.copy(cr, uid, proj.id, default = {
373 'name':_("%s (copy)") % (proj.name),
375 'date_start':new_date_start,
377 'parent_id':parent_id}, context=context)
378 result.append(new_id)
380 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)], context=context)
381 parent_id = self.read(cr, uid, new_id, ['analytic_account_id'])['analytic_account_id'][0]
383 self.duplicate_template(cr, uid, child_ids, context={'parent_id': parent_id})
385 if result and len(result):
387 form_view_id = data_obj._get_id(cr, uid, 'project', 'edit_project')
388 form_view = data_obj.read(cr, uid, form_view_id, ['res_id'])
389 tree_view_id = data_obj._get_id(cr, uid, 'project', 'view_project')
390 tree_view = data_obj.read(cr, uid, tree_view_id, ['res_id'])
391 search_view_id = data_obj._get_id(cr, uid, 'project', 'view_project_project_filter')
392 search_view = data_obj.read(cr, uid, search_view_id, ['res_id'])
394 'name': _('Projects'),
396 'view_mode': 'form,tree',
397 'res_model': 'project.project',
400 'views': [(form_view['res_id'],'form'),(tree_view['res_id'],'tree')],
401 'type': 'ir.actions.act_window',
402 'search_view_id': search_view['res_id'],
406 # set active value for a project, its sub projects and its tasks
407 def setActive(self, cr, uid, ids, value=True, context=None):
408 task_obj = self.pool.get('project.task')
409 for proj in self.browse(cr, uid, ids, context=None):
410 self.write(cr, uid, [proj.id], {'state': value and 'open' or 'template'}, context)
411 cr.execute('select id from project_task where project_id=%s', (proj.id,))
412 tasks_id = [x[0] for x in cr.fetchall()]
414 task_obj.write(cr, uid, tasks_id, {'active': value}, context=context)
415 child_ids = self.search(cr, uid, [('parent_id','=', proj.analytic_account_id.id)])
417 self.setActive(cr, uid, child_ids, value, context=None)
420 def _schedule_header(self, cr, uid, ids, force_members=True, context=None):
421 context = context or {}
422 if type(ids) in (long, int,):
424 projects = self.browse(cr, uid, ids, context=context)
426 for project in projects:
427 if (not project.members) and force_members:
428 raise osv.except_osv(_('Warning!'),_("You must assign members on the project '%s'!") % (project.name,))
430 resource_pool = self.pool.get('resource.resource')
432 result = "from openerp.addons.resource.faces import *\n"
433 result += "import datetime\n"
434 for project in self.browse(cr, uid, ids, context=context):
435 u_ids = [i.id for i in project.members]
436 if project.user_id and (project.user_id.id not in u_ids):
437 u_ids.append(project.user_id.id)
438 for task in project.tasks:
439 if task.user_id and (task.user_id.id not in u_ids):
440 u_ids.append(task.user_id.id)
441 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
442 resource_objs = resource_pool.generate_resources(cr, uid, u_ids, calendar_id, context=context)
443 for key, vals in resource_objs.items():
445 class User_%s(Resource):
447 ''' % (key, vals.get('efficiency', False))
454 def _schedule_project(self, cr, uid, project, context=None):
455 resource_pool = self.pool.get('resource.resource')
456 calendar_id = project.resource_calendar_id and project.resource_calendar_id.id or False
457 working_days = resource_pool.compute_working_calendar(cr, uid, calendar_id, context=context)
458 # TODO: check if we need working_..., default values are ok.
459 puids = [x.id for x in project.members]
461 puids.append(project.user_id.id)
469 project.date_start or time.strftime('%Y-%m-%d'), working_days,
470 '|'.join(['User_'+str(x) for x in puids])
472 vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False
479 #TODO: DO Resource allocation and compute availability
480 def compute_allocation(self, rc, uid, ids, start_date, end_date, context=None):
486 def schedule_tasks(self, cr, uid, ids, context=None):
487 context = context or {}
488 if type(ids) in (long, int,):
490 projects = self.browse(cr, uid, ids, context=context)
491 result = self._schedule_header(cr, uid, ids, False, context=context)
492 for project in projects:
493 result += self._schedule_project(cr, uid, project, context=context)
494 result += self.pool.get('project.task')._generate_task(cr, uid, project.tasks, ident=4, context=context)
497 exec result in local_dict
498 projects_gantt = Task.BalancedProject(local_dict['Project'])
500 for project in projects:
501 project_gantt = getattr(projects_gantt, 'Project_%d' % (project.id,))
502 for task in project.tasks:
503 if task.stage_id and task.stage_id.fold:
506 p = getattr(project_gantt, 'Task_%d' % (task.id,))
508 self.pool.get('project.task').write(cr, uid, [task.id], {
509 'date_start': p.start.strftime('%Y-%m-%d %H:%M:%S'),
510 'date_end': p.end.strftime('%Y-%m-%d %H:%M:%S')
512 if (not task.user_id) and (p.booked_resource):
513 self.pool.get('project.task').write(cr, uid, [task.id], {
514 'user_id': int(p.booked_resource[0].name[5:]),
518 def create(self, cr, uid, vals, context=None):
521 # Prevent double project creation when 'use_tasks' is checked + alias management
522 create_context = dict(context, project_creation_in_progress=True,
523 alias_model_name=vals.get('alias_model', 'project.task'),
524 alias_parent_model_name=self._name)
526 if vals.get('type', False) not in ('template', 'contract'):
527 vals['type'] = 'contract'
529 project_id = super(project, self).create(cr, uid, vals, context=create_context)
530 project_rec = self.browse(cr, uid, project_id, context=context)
531 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)
534 def write(self, cr, uid, ids, vals, context=None):
535 # if alias_model has been changed, update alias_model_id accordingly
536 if vals.get('alias_model'):
537 model_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', vals.get('alias_model', 'project.task'))])
538 vals.update(alias_model_id=model_ids[0])
539 return super(project, self).write(cr, uid, ids, vals, context=context)
543 _name = "project.task"
544 _description = "Task"
545 _date_name = "date_start"
546 _inherit = ['mail.thread', 'ir.needaction_mixin']
548 _mail_post_access = 'read'
551 'project.mt_task_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence == 1,
552 'project.mt_task_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id.sequence != 1,
555 'project.mt_task_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id,
558 'project.mt_task_blocked': lambda self, cr, uid, obj, ctx=None: obj.kanban_state == 'blocked',
562 def _get_default_partner(self, cr, uid, context=None):
563 project_id = self._get_default_project_id(cr, uid, context)
565 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
566 if project and project.partner_id:
567 return project.partner_id.id
570 def _get_default_project_id(self, cr, uid, context=None):
571 """ Gives default section by checking if present in the context """
572 return (self._resolve_project_id_from_context(cr, uid, context=context) or False)
574 def _get_default_stage_id(self, cr, uid, context=None):
575 """ Gives default stage_id """
576 project_id = self._get_default_project_id(cr, uid, context=context)
577 return self.stage_find(cr, uid, [], project_id, [('sequence', '=', '1')], context=context)
579 def _resolve_project_id_from_context(self, cr, uid, context=None):
580 """ Returns ID of project based on the value of 'default_project_id'
581 context key, or None if it cannot be resolved to a single
586 if type(context.get('default_project_id')) in (int, long):
587 return context['default_project_id']
588 if isinstance(context.get('default_project_id'), basestring):
589 project_name = context['default_project_id']
590 project_ids = self.pool.get('project.project').name_search(cr, uid, name=project_name, context=context)
591 if len(project_ids) == 1:
592 return project_ids[0][0]
595 def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
596 stage_obj = self.pool.get('project.task.type')
597 order = stage_obj._order
598 access_rights_uid = access_rights_uid or uid
599 if read_group_order == 'stage_id desc':
600 order = '%s desc' % order
602 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
604 search_domain += ['|', ('project_ids', '=', project_id)]
605 search_domain += [('id', 'in', ids)]
606 stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
607 result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
608 # restore order of the search
609 result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))
612 for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context):
613 fold[stage.id] = stage.fold or False
616 def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None):
617 res_users = self.pool.get('res.users')
618 project_id = self._resolve_project_id_from_context(cr, uid, context=context)
619 access_rights_uid = access_rights_uid or uid
621 ids += self.pool.get('project.project').read(cr, access_rights_uid, project_id, ['members'], context=context)['members']
622 order = res_users._order
623 # lame way to allow reverting search, should just work in the trivial case
624 if read_group_order == 'user_id desc':
625 order = '%s desc' % order
626 # de-duplicate and apply search order
627 ids = res_users._search(cr, uid, [('id','in',ids)], order=order, access_rights_uid=access_rights_uid, context=context)
628 result = res_users.name_get(cr, access_rights_uid, ids, context=context)
629 # restore order of the search
630 result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0])))
634 'stage_id': _read_group_stage_ids,
635 'user_id': _read_group_user_id,
638 def _str_get(self, task, level=0, border='***', context=None):
639 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'+ \
640 border[0]+' '+(task.name or '')+'\n'+ \
641 (task.description or '')+'\n\n'
643 # Compute: effective_hours, total_hours, progress
644 def _hours_get(self, cr, uid, ids, field_names, args, context=None):
646 cr.execute("SELECT task_id, COALESCE(SUM(hours),0) FROM project_task_work WHERE task_id IN %s GROUP BY task_id",(tuple(ids),))
647 hours = dict(cr.fetchall())
648 for task in self.browse(cr, uid, ids, context=context):
649 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)}
650 res[task.id]['delay_hours'] = res[task.id]['total_hours'] - task.planned_hours
651 res[task.id]['progress'] = 0.0
652 if (task.remaining_hours + hours.get(task.id, 0.0)):
653 res[task.id]['progress'] = round(min(100.0 * hours.get(task.id, 0.0) / res[task.id]['total_hours'], 99.99),2)
654 # TDE CHECK: if task.state in ('done','cancelled'):
655 if task.stage_id and task.stage_id.fold:
656 res[task.id]['progress'] = 100.0
659 def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned=0.0):
660 if remaining and not planned:
661 return {'value': {'planned_hours': remaining}}
664 def onchange_planned(self, cr, uid, ids, planned=0.0, effective=0.0):
665 return {'value': {'remaining_hours': planned - effective}}
667 def onchange_project(self, cr, uid, id, project_id, context=None):
669 project = self.pool.get('project.project').browse(cr, uid, project_id, context=context)
670 if project and project.partner_id:
671 return {'value': {'partner_id': project.partner_id.id}}
674 def onchange_user_id(self, cr, uid, ids, user_id, context=None):
677 vals['date_start'] = fields.datetime.now()
678 return {'value': vals}
680 def duplicate_task(self, cr, uid, map_ids, context=None):
681 for new in map_ids.values():
682 task = self.browse(cr, uid, new, context)
683 child_ids = [ ch.id for ch in task.child_ids]
685 for child in task.child_ids:
686 if child.id in map_ids.keys():
687 child_ids.remove(child.id)
688 child_ids.append(map_ids[child.id])
690 parent_ids = [ ch.id for ch in task.parent_ids]
692 for parent in task.parent_ids:
693 if parent.id in map_ids.keys():
694 parent_ids.remove(parent.id)
695 parent_ids.append(map_ids[parent.id])
696 #FIXME why there is already the copy and the old one
697 self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(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 _is_template(self, cr, uid, ids, field_name, arg, context=None):
716 for task in self.browse(cr, uid, ids, context=context):
719 if task.project_id.active == False or task.project_id.state == 'template':
723 def _get_task(self, cr, uid, ids, context=None):
725 for work in self.pool.get('project.task.work').browse(cr, uid, ids, context=context):
726 if work.task_id: result[work.task_id.id] = True
730 '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."),
731 'name': fields.char('Task Summary', size=128, required=True, select=True),
732 'description': fields.text('Description'),
733 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
734 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),
735 'stage_id': fields.many2one('project.task.type', 'Stage', track_visibility='onchange',
736 domain="[('project_ids', '=', project_id)]"),
737 'categ_ids': fields.many2many('project.category', string='Tags'),
738 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State',
739 track_visibility='onchange',
740 help="A task's kanban state indicates special situations affecting it:\n"
741 " * Normal is the default situation\n"
742 " * Blocked indicates something is preventing the progress of this task\n"
743 " * Ready for next stage indicates the task is ready to be pulled to the next stage",
744 readonly=True, required=False),
745 'create_date': fields.datetime('Create Date', readonly=True,select=True),
746 'date_start': fields.datetime('Starting Date',select=True),
747 'date_end': fields.datetime('Ending Date',select=True),
748 'date_deadline': fields.date('Deadline',select=True),
749 'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
750 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select="1", track_visibility='onchange'),
751 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'),
752 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'),
753 'notes': fields.text('Notes'),
754 '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.'),
755 'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='hours', help="Computed using the sum of the task work done.",
757 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
758 'project.task.work': (_get_task, ['hours'], 10),
760 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."),
761 'total_hours': fields.function(_hours_get, string='Total', multi='hours', help="Computed as: Time Spent + Remaining Time.",
763 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
764 'project.task.work': (_get_task, ['hours'], 10),
766 '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",
768 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours','state'], 10),
769 'project.task.work': (_get_task, ['hours'], 10),
771 '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.",
773 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['work_ids', 'remaining_hours', 'planned_hours'], 10),
774 'project.task.work': (_get_task, ['hours'], 10),
776 'user_id': fields.many2one('res.users', 'Assigned to', track_visibility='onchange'),
777 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'),
778 'partner_id': fields.many2one('res.partner', 'Customer'),
779 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'),
780 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'),
781 'company_id': fields.many2one('res.company', 'Company'),
782 'id': fields.integer('ID', readonly=True),
783 'color': fields.integer('Color Index'),
784 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
787 'stage_id': _get_default_stage_id,
788 'project_id': _get_default_project_id,
789 'date_last_stage_update': lambda *a: fields.datetime.now(),
790 'kanban_state': 'normal',
795 'user_id': lambda obj, cr, uid, ctx=None: uid,
796 'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=ctx),
797 'partner_id': lambda self, cr, uid, ctx=None: self._get_default_partner(cr, uid, context=ctx),
799 _order = "priority, sequence, date_start, name, id"
801 def set_high_priority(self, cr, uid, ids, *args):
802 """Set task priority to high
804 return self.write(cr, uid, ids, {'priority' : '0'})
806 def set_normal_priority(self, cr, uid, ids, *args):
807 """Set task priority to normal
809 return self.write(cr, uid, ids, {'priority' : '2'})
811 def _check_recursion(self, cr, uid, ids, context=None):
813 visited_branch = set()
815 res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context)
821 def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None):
822 if id in visited_branch: #Cycle
825 if id in visited_node: #Already tested don't work one more time for nothing
828 visited_branch.add(id)
831 #visit child using DFS
832 task = self.browse(cr, uid, id, context=context)
833 for child in task.child_ids:
834 res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context)
838 visited_branch.remove(id)
841 def _check_dates(self, cr, uid, ids, context=None):
844 obj_task = self.browse(cr, uid, ids[0], context=context)
845 start = obj_task.date_start or False
846 end = obj_task.date_end or False
853 (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']),
854 (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end'])
857 # Override view according to the company definition
858 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
859 users_obj = self.pool.get('res.users')
860 if context is None: context = {}
861 # read uom as admin to avoid access rights issues, e.g. for portal/share users,
862 # this should be safe (no context passed to avoid side-effects)
863 obj_tm = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id
864 tm = obj_tm and obj_tm.name or 'Hours'
866 res = super(task, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu=submenu)
868 if tm in ['Hours','Hour']:
871 eview = etree.fromstring(res['arch'])
873 def _check_rec(eview):
874 if eview.attrib.get('widget','') == 'float_time':
875 eview.set('widget','float')
882 res['arch'] = etree.tostring(eview)
884 for f in res['fields']:
885 if 'Hours' in res['fields'][f]['string']:
886 res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm)
889 def get_empty_list_help(self, cr, uid, help, context=None):
890 context['empty_list_help_id'] = context.get('default_project_id')
891 context['empty_list_help_model'] = 'project.project'
892 context['empty_list_help_document_name'] = _("tasks")
893 return super(task, self).get_empty_list_help(cr, uid, help, context=context)
895 # ----------------------------------------
897 # ----------------------------------------
899 def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
900 """ Override of the base.stage method
901 Parameter of the stage search taken from the lead:
902 - section_id: if set, stages must belong to this section or
903 be a default stage; if not set, stages must be default
906 if isinstance(cases, (int, long)):
907 cases = self.browse(cr, uid, cases, context=context)
908 # collect all section_ids
911 section_ids.append(section_id)
914 section_ids.append(task.project_id.id)
917 search_domain = [('|')] * (len(section_ids) - 1)
918 for section_id in section_ids:
919 search_domain.append(('project_ids', '=', section_id))
920 search_domain += list(domain)
921 # perform search, return the first found
922 stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context)
927 def _check_child_task(self, cr, uid, ids, context=None):
930 tasks = self.browse(cr, uid, ids, context=context)
933 for child in task.child_ids:
934 if child.stage_id and not child.stage_id.fold:
935 raise osv.except_osv(_("Warning!"), _("Child task still open.\nPlease cancel or complete child task first."))
938 def _delegate_task_attachments(self, cr, uid, task_id, delegated_task_id, context=None):
939 attachment = self.pool.get('ir.attachment')
940 attachment_ids = attachment.search(cr, uid, [('res_model', '=', self._name), ('res_id', '=', task_id)], context=context)
941 new_attachment_ids = []
942 for attachment_id in attachment_ids:
943 new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context))
944 return new_attachment_ids
946 def do_delegate(self, cr, uid, ids, delegate_data=None, context=None):
948 Delegate Task to another users.
950 if delegate_data is None:
952 assert delegate_data['user_id'], _("Delegated User should be specified")
954 for task in self.browse(cr, uid, ids, context=context):
955 delegated_task_id = self.copy(cr, uid, task.id, {
956 'name': delegate_data['name'],
957 'project_id': delegate_data['project_id'] and delegate_data['project_id'][0] or False,
958 'stage_id': delegate_data.get('stage_id') and delegate_data.get('stage_id')[0] or False,
959 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False,
960 'planned_hours': delegate_data['planned_hours'] or 0.0,
961 'parent_ids': [(6, 0, [task.id])],
962 'description': delegate_data['new_task_description'] or '',
966 self._delegate_task_attachments(cr, uid, task.id, delegated_task_id, context=context)
967 newname = delegate_data['prefix'] or ''
969 'remaining_hours': delegate_data['planned_hours_me'],
970 'planned_hours': delegate_data['planned_hours_me'] + (task.effective_hours or 0.0),
973 delegated_tasks[task.id] = delegated_task_id
974 return delegated_tasks
976 def set_remaining_time(self, cr, uid, ids, remaining_time=1.0, context=None):
977 for task in self.browse(cr, uid, ids, context=context):
978 if (task.stage_id and task.stage_id.sequence == 1) or (task.planned_hours == 0.0):
979 self.write(cr, uid, [task.id], {'planned_hours': remaining_time}, context=context)
980 self.write(cr, uid, ids, {'remaining_hours': remaining_time}, context=context)
983 def set_remaining_time_1(self, cr, uid, ids, context=None):
984 return self.set_remaining_time(cr, uid, ids, 1.0, context)
986 def set_remaining_time_2(self, cr, uid, ids, context=None):
987 return self.set_remaining_time(cr, uid, ids, 2.0, context)
989 def set_remaining_time_5(self, cr, uid, ids, context=None):
990 return self.set_remaining_time(cr, uid, ids, 5.0, context)
992 def set_remaining_time_10(self, cr, uid, ids, context=None):
993 return self.set_remaining_time(cr, uid, ids, 10.0, context)
995 def set_kanban_state_blocked(self, cr, uid, ids, context=None):
996 return self.write(cr, uid, ids, {'kanban_state': 'blocked'}, context=context)
998 def set_kanban_state_normal(self, cr, uid, ids, context=None):
999 return self.write(cr, uid, ids, {'kanban_state': 'normal'}, context=context)
1001 def set_kanban_state_done(self, cr, uid, ids, context=None):
1002 self.write(cr, uid, ids, {'kanban_state': 'done'}, context=context)
1005 def _store_history(self, cr, uid, ids, context=None):
1006 for task in self.browse(cr, uid, ids, context=context):
1007 self.pool.get('project.task.history').create(cr, uid, {
1009 'remaining_hours': task.remaining_hours,
1010 'planned_hours': task.planned_hours,
1011 'kanban_state': task.kanban_state,
1012 'type_id': task.stage_id.id,
1013 'user_id': task.user_id.id
1018 # ------------------------------------------------
1020 # ------------------------------------------------
1022 def create(self, cr, uid, vals, context=None):
1027 if vals.get('project_id') and not context.get('default_project_id'):
1028 context['default_project_id'] = vals.get('project_id')
1029 # user_id change: update date_start
1030 if vals.get('user_id'):
1031 vals['date_start'] = fields.datetime.now()
1033 # context: no_log, because subtype already handle this
1034 create_context = dict(context, mail_create_nolog=True)
1035 task_id = super(task, self).create(cr, uid, vals, context=create_context)
1036 self._store_history(cr, uid, [task_id], context=context)
1039 def write(self, cr, uid, ids, vals, context=None):
1040 if isinstance(ids, (int, long)):
1043 # stage change: update date_last_stage_update
1044 if 'stage_id' in vals:
1045 vals['date_last_stage_update'] = fields.datetime.now()
1046 # user_id change: update date_start
1047 if vals.get('user_id'):
1048 vals['date_start'] = fields.datetime.now()
1050 # Overridden to reset the kanban_state to normal whenever
1051 # the stage (stage_id) of the task changes.
1052 if vals and not 'kanban_state' in vals and 'stage_id' in vals:
1053 new_stage = vals.get('stage_id')
1054 vals_reset_kstate = dict(vals, kanban_state='normal')
1055 for t in self.browse(cr, uid, ids, context=context):
1056 write_vals = vals_reset_kstate if t.stage_id != new_stage else vals
1057 super(task, self).write(cr, uid, [t.id], write_vals, context=context)
1060 result = super(task, self).write(cr, uid, ids, vals, context=context)
1062 if any(item in vals for item in ['stage_id', 'remaining_hours', 'user_id', 'kanban_state']):
1063 self._store_history(cr, uid, ids, context=context)
1066 def unlink(self, cr, uid, ids, context=None):
1069 self._check_child_task(cr, uid, ids, context=context)
1070 res = super(task, self).unlink(cr, uid, ids, context)
1073 def _generate_task(self, cr, uid, tasks, ident=4, context=None):
1074 context = context or {}
1078 if task.stage_id and task.stage_id.fold:
1083 %s effort = \"%.2fH\"''' % (ident,task.id, ident,task.remaining_hours, ident,task.total_hours)
1085 for t2 in task.parent_ids:
1086 start.append("up.Task_%s.end" % (t2.id,))
1090 ''' % (ident,','.join(start))
1095 ''' % (ident, 'User_'+str(task.user_id.id))
1100 # ---------------------------------------------------
1102 # ---------------------------------------------------
1104 def message_get_reply_to(self, cr, uid, ids, context=None):
1105 """ Override to get the reply_to of the parent project. """
1106 return [task.project_id.message_get_reply_to()[0] if task.project_id else False
1107 for task in self.browse(cr, uid, ids, context=context)]
1109 def message_new(self, cr, uid, msg, custom_values=None, context=None):
1110 """ Override to updates the document according to the email. """
1111 if custom_values is None: custom_values = {}
1113 'name': msg.get('subject'),
1114 'planned_hours': 0.0,
1116 defaults.update(custom_values)
1117 return super(task, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
1119 def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):
1120 """ Override to update the task according to the email. """
1121 if update_vals is None: update_vals = {}
1124 'cost':'planned_hours',
1126 for line in msg['body'].split('\n'):
1128 res = tools.command_re.match(line)
1130 match = res.group(1).lower()
1131 field = maps.get(match)
1134 update_vals[field] = float(res.group(2).lower())
1135 except (ValueError, TypeError):
1138 getattr(self,act)(cr, uid, ids, context=context)
1139 return super(task,self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
1141 class project_work(osv.osv):
1142 _name = "project.task.work"
1143 _description = "Project Task Work"
1145 'name': fields.char('Work summary', size=128),
1146 'date': fields.datetime('Date', select="1"),
1147 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select="1"),
1148 'hours': fields.float('Time Spent'),
1149 'user_id': fields.many2one('res.users', 'Done by', required=True, select="1"),
1150 'company_id': fields.related('task_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True)
1154 'user_id': lambda obj, cr, uid, context: uid,
1155 'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S')
1158 _order = "date desc"
1159 def create(self, cr, uid, vals, *args, **kwargs):
1160 if 'hours' in vals and (not vals['hours']):
1161 vals['hours'] = 0.00
1162 if 'task_id' in vals:
1163 cr.execute('update project_task set remaining_hours=remaining_hours - %s where id=%s', (vals.get('hours',0.0), vals['task_id']))
1164 return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
1166 def write(self, cr, uid, ids, vals, context=None):
1167 if 'hours' in vals and (not vals['hours']):
1168 vals['hours'] = 0.00
1170 for work in self.browse(cr, uid, ids, context=context):
1171 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))
1172 return super(project_work,self).write(cr, uid, ids, vals, context)
1174 def unlink(self, cr, uid, ids, *args, **kwargs):
1175 for work in self.browse(cr, uid, ids):
1176 cr.execute('update project_task set remaining_hours=remaining_hours + %s where id=%s', (work.hours, work.task_id.id))
1177 return super(project_work,self).unlink(cr, uid, ids,*args, **kwargs)
1180 class account_analytic_account(osv.osv):
1181 _inherit = 'account.analytic.account'
1182 _description = 'Analytic Account'
1184 '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"),
1185 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'),
1188 def on_change_template(self, cr, uid, ids, template_id, context=None):
1189 res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context)
1190 if template_id and 'value' in res:
1191 template = self.browse(cr, uid, template_id, context=context)
1192 res['value']['use_tasks'] = template.use_tasks
1195 def _trigger_project_creation(self, cr, uid, vals, context=None):
1197 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.
1199 if context is None: context = {}
1200 return vals.get('use_tasks') and not 'project_creation_in_progress' in context
1202 def project_create(self, cr, uid, analytic_account_id, vals, context=None):
1204 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.
1206 project_pool = self.pool.get('project.project')
1207 project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)])
1208 if not project_id and self._trigger_project_creation(cr, uid, vals, context=context):
1210 'name': vals.get('name'),
1211 'analytic_account_id': analytic_account_id,
1212 'type': vals.get('type','contract'),
1214 return project_pool.create(cr, uid, project_values, context=context)
1217 def create(self, cr, uid, vals, context=None):
1220 if vals.get('child_ids', False) and context.get('analytic_project_copy', False):
1221 vals['child_ids'] = []
1222 analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context)
1223 self.project_create(cr, uid, analytic_account_id, vals, context=context)
1224 return analytic_account_id
1226 def write(self, cr, uid, ids, vals, context=None):
1227 vals_for_project = vals.copy()
1228 for account in self.browse(cr, uid, ids, context=context):
1229 if not vals.get('name'):
1230 vals_for_project['name'] = account.name
1231 if not vals.get('type'):
1232 vals_for_project['type'] = account.type
1233 self.project_create(cr, uid, account.id, vals_for_project, context=context)
1234 return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context)
1236 def unlink(self, cr, uid, ids, *args, **kwargs):
1237 project_obj = self.pool.get('project.project')
1238 analytic_ids = project_obj.search(cr, uid, [('analytic_account_id','in',ids)])
1240 raise osv.except_osv(_('Warning!'), _('Please delete the project linked with this account first.'))
1241 return super(account_analytic_account, self).unlink(cr, uid, ids, *args, **kwargs)
1243 class project_project(osv.osv):
1244 _inherit = 'project.project'
1249 class project_task_history(osv.osv):
1251 Tasks History, used for cumulative flow charts (Lean/Agile)
1253 _name = 'project.task.history'
1254 _description = 'History of Tasks'
1255 _rec_name = 'task_id'
1258 def _get_date(self, cr, uid, ids, name, arg, context=None):
1260 for history in self.browse(cr, uid, ids, context=context):
1261 if history.type_id and history.type_id.fold:
1262 result[history.id] = history.date
1264 cr.execute('''select
1267 project_task_history
1271 order by id limit 1''', (history.task_id.id, history.id))
1273 result[history.id] = res and res[0] or False
1276 def _get_related_date(self, cr, uid, ids, context=None):
1278 for history in self.browse(cr, uid, ids, context=context):
1279 cr.execute('''select
1282 project_task_history
1286 order by id desc limit 1''', (history.task_id.id, history.id))
1289 result.append(res[0])
1293 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True),
1294 'type_id': fields.many2one('project.task.type', 'Stage'),
1295 'kanban_state': fields.selection([('normal', 'Normal'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], 'Kanban State', required=False),
1296 'date': fields.date('Date', select=True),
1297 'end_date': fields.function(_get_date, string='End Date', type="date", store={
1298 'project.task.history': (_get_related_date, None, 20)
1300 'remaining_hours': fields.float('Remaining Time', digits=(16, 2)),
1301 'planned_hours': fields.float('Planned Time', digits=(16, 2)),
1302 'user_id': fields.many2one('res.users', 'Responsible'),
1305 'date': fields.date.context_today,
1308 class project_task_history_cumulative(osv.osv):
1309 _name = 'project.task.history.cumulative'
1310 _table = 'project_task_history_cumulative'
1311 _inherit = 'project.task.history'
1315 'end_date': fields.date('End Date'),
1316 'project_id': fields.many2one('project.project', 'Project'),
1320 tools.drop_view_if_exists(cr, 'project_task_history_cumulative')
1322 cr.execute(""" CREATE VIEW project_task_history_cumulative AS (
1324 history.date::varchar||'-'||history.history_id::varchar AS id,
1325 history.date AS end_date,
1330 h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date,
1331 h.task_id, h.type_id, h.user_id, h.kanban_state,
1332 greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours,
1335 project_task_history AS h
1336 JOIN project_task AS t ON (h.task_id = t.id)
1342 class project_category(osv.osv):
1343 """ Category of project's task (or issue) """
1344 _name = "project.category"
1345 _description = "Category of project's task, issue, ..."
1347 'name': fields.char('Name', size=64, required=True, translate=True),
1349 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: