X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fproject%2Fproject.py;h=afe507c393921bd3e1f2439029c7675366922048;hb=6073fdc4258e44c5f1a868b4d8eaed64bbd42520;hp=ae70dcc1dd460849ab49faf1d2730515364b1f37;hpb=13659d0590be8766c600eeba5616a164ca3b575b;p=odoo%2Fodoo.git diff --git a/addons/project/project.py b/addons/project/project.py index ae70dcc..afe507c 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -19,12 +19,14 @@ # ############################################################################## -from base_status.base_stage import base_stage -from datetime import datetime, date +import time from lxml import etree +from datetime import datetime, date + +import tools +from base_status.base_stage import base_stage from osv import fields, osv from openerp.addons.resource.faces import task as Task -import time from tools.translate import _ from openerp import SUPERUSER_ID @@ -41,19 +43,20 @@ class project_task_type(osv.osv): 'case_default': fields.boolean('Common to All Projects', help="If you check this field, this stage will be proposed by default on each new project. It will not assign this stage to existing projects."), 'project_ids': fields.many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', 'Projects'), - 'state': fields.selection(_TASK_STATE, 'State', required=True, - help="The related state for the stage. The state of your document will automatically change regarding the selected stage. Example, a stage is related to the state 'Close', when your document reach this stage, it will be automatically closed."), + 'state': fields.selection(_TASK_STATE, 'Related Status', required=True, + help="The status of your document is automatically changed regarding the selected stage. " \ + "For example, if a stage is related to the status 'Close', when your document reaches this stage, it is automatically closed."), 'fold': fields.boolean('Hide in views if empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."), } _defaults = { 'sequence': 1, - 'state': 'draft', + 'state': 'open', 'fold': False, + 'case_default': True, } _order = 'sequence' - def short_name(name): """Keep first word(s) of name to make it small enough but distinctive""" @@ -163,19 +166,30 @@ class project(osv.osv): res[id]['progress_rate'] = 0.0 return res - def unlink(self, cr, uid, ids, *args, **kwargs): + def unlink(self, cr, uid, ids, context=None): alias_ids = [] mail_alias = self.pool.get('mail.alias') - for proj in self.browse(cr, uid, ids): + for proj in self.browse(cr, uid, ids, context=context): if proj.tasks: raise osv.except_osv(_('Invalid Action!'), _('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.')) elif proj.alias_id: alias_ids.append(proj.alias_id.id) - res = super(project, self).unlink(cr, uid, ids, *args, **kwargs) - mail_alias.unlink(cr, uid, alias_ids, *args, **kwargs) + res = super(project, self).unlink(cr, uid, ids, context=context) + mail_alias.unlink(cr, uid, alias_ids, context=context) return res - + + def _get_attached_docs(self, cr, uid, ids, field_name, arg, context): + res = {} + attachment = self.pool.get('ir.attachment') + task = self.pool.get('project.task') + for id in ids: + project_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', 'in', [id])], context=context, count=True) + task_ids = task.search(cr, uid, [('project_id', 'in', [id])], context=context) + task_attachments = attachment.search(cr, uid, [('res_model', '=', 'project.task'), ('res_id', 'in', task_ids)], context=context, count=True) + res[id] = project_attachments or 0 + task_attachments or 0 + return res + def _task_count(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, 0) task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)]) @@ -186,10 +200,28 @@ class project(osv.osv): def _get_alias_models(self, cr, uid, context=None): """Overriden in project_issue to offer more options""" return [('project.task', "Tasks")] - + + def attachment_tree_view(self, cr, uid, ids, context): + task_ids = self.pool.get('project.task').search(cr, uid, [('project_id', 'in', ids)]) + domain = [ + '|', + '&', ('res_model', '=', 'project.project'), ('res_id', 'in', ids), + '&', ('res_model', '=', 'project.task'), ('res_id', 'in', task_ids) + ] + res_id = ids and ids[0] or False + return { + 'name': _('Attachments'), + 'domain': domain, + 'res_model': 'ir.attachment', + 'type': 'ir.actions.act_window', + 'view_id': False, + 'view_mode': 'tree,form', + 'view_type': 'form', + 'limit': 80, + 'context': "{'default_res_model': '%s','default_res_id': %d}" % (self._name, res_id) + } # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field _alias_models = lambda self, *args, **kwargs: self._get_alias_models(*args, **kwargs) - _columns = { 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250), 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."), @@ -230,6 +262,7 @@ class project(osv.osv): help="The kind of document created when an email is received on this project's email alias"), 'privacy_visibility': fields.selection([('public','Public'), ('followers','Followers Only')], 'Privacy / Visibility', required=True), 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,), + 'doc_count':fields.function(_get_attached_docs, string="Number of documents attached", type='int') } def _get_type_common(self, cr, uid, context): @@ -309,11 +342,12 @@ class project(osv.osv): task_obj.duplicate_task(cr, uid, map_task_id, context=context) return True - def copy(self, cr, uid, id, default={}, context=None): + def copy(self, cr, uid, id, default=None, context=None): if context is None: context = {} + if default is None: + default = {} - default = default or {} context['active_test'] = False default['state'] = 'open' default['tasks'] = [] @@ -321,7 +355,7 @@ class project(osv.osv): default.pop('alias_id', None) proj = self.browse(cr, uid, id, context=context) if not default.get('name', False): - default['name'] = proj.name + _(' (copy)') + default.update(name=_("%s (copy)") % (proj.name)) res = super(project, self).copy(cr, uid, id, default, context) self.map_tasks(cr,uid,id,res,context) return res @@ -342,7 +376,7 @@ class project(osv.osv): new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d') context.update({'copy':True}) new_id = self.copy(cr, uid, proj.id, default = { - 'name': proj.name +_(' (copy)'), + 'name':_("%s (copy)") % (proj.name), 'state':'open', 'date_start':new_date_start, 'date':new_date_end, @@ -440,7 +474,7 @@ def Project(): resource = %s """ % ( project.id, - project.date_start, working_days, + project.date_start or time.strftime('%Y-%m-%d'), working_days, '|'.join(['User_'+str(x) for x in puids]) ) vacation = calendar_id and tuple(resource_pool.compute_vacation(cr, uid, calendar_id, context=context)) or False @@ -498,7 +532,7 @@ def Project(): # Prevent double project creation when 'use_tasks' is checked! context = dict(context, project_creation_in_progress=True) mail_alias = self.pool.get('mail.alias') - if not vals.get('alias_id'): + if not vals.get('alias_id') and vals.get('name', False): vals.pop('alias_name', None) # prevent errors during copy() alias_id = mail_alias.create_unique_alias(cr, uid, # Using '+' allows using subaddressing for those who don't @@ -507,29 +541,26 @@ def Project(): model_name=vals.get('alias_model', 'project.task'), context=context) vals['alias_id'] = alias_id + vals['type'] = 'contract' project_id = super(project, self).create(cr, uid, vals, context) mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'project_id': project_id} }, context) self.create_send_note(cr, uid, [project_id], context=context) return project_id def create_send_note(self, cr, uid, ids, context=None): - return self.message_post(cr, uid, ids, body=_("Project has been created."), subtype="new", context=context) + return self.message_post(cr, uid, ids, body=_("Project has been created."), context=context) def set_open_send_note(self, cr, uid, ids, context=None): - message = _("Project has been opened.") - return self.message_post(cr, uid, ids, body=message, subtype="open", context=context) + return self.message_post(cr, uid, ids, body=_("Project has been opened."), context=context) def set_pending_send_note(self, cr, uid, ids, context=None): - message = _("Project is now pending.") - return self.message_post(cr, uid, ids, body=message, subtype="pending", context=context) + return self.message_post(cr, uid, ids, body=_("Project is now pending."), context=context) def set_cancel_send_note(self, cr, uid, ids, context=None): - message = _("Project has been cancelled.") - return self.message_post(cr, uid, ids, body=message, subtype="cancel", context=context) + return self.message_post(cr, uid, ids, body=_("Project has been canceled."), context=context) def set_close_send_note(self, cr, uid, ids, context=None): - message = _("Project has been closed.") - return self.message_post(cr, uid, ids, body=message, subtype="close", context=context) + return self.message_post(cr, uid, ids, body=_("Project has been closed."), context=context) def write(self, cr, uid, ids, vals, context=None): # if alias_model has been changed, update alias_model_id accordingly @@ -582,13 +613,17 @@ class task(base_stage, osv.osv): search_domain = [] project_id = self._resolve_project_id_from_context(cr, uid, context=context) if project_id: - search_domain += ['|', '&', ('project_ids', '=', project_id), ('fold', '=', False)] - search_domain += ['|', ('id', 'in', ids), '&', ('case_default', '=', True), ('fold', '=', False)] + search_domain += ['|', ('project_ids', '=', project_id)] + search_domain += ['|', ('id', 'in', ids), ('case_default', '=', True)] stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context) result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context) # restore order of the search result.sort(lambda x,y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) - return result + + fold = {} + for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context): + fold[stage.id] = stage.fold or False + return result, fold def _read_group_user_id(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None): res_users = self.pool.get('res.users') @@ -605,7 +640,7 @@ class task(base_stage, osv.osv): result = res_users.name_get(cr, access_rights_uid, ids, context=context) # restore order of the search result.sort(lambda x,y: cmp(ids.index(x[0]), ids.index(y[0]))) - return result + return result, {} _group_by_full = { 'stage_id': _read_group_stage_ids, @@ -642,12 +677,12 @@ class task(base_stage, osv.osv): res[task.id]['progress'] = 100.0 return res - def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned = 0.0): + def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned=0.0): if remaining and not planned: return {'value':{'planned_hours': remaining}} return {} - def onchange_planned(self, cr, uid, ids, planned = 0.0, effective = 0.0): + def onchange_planned(self, cr, uid, ids, planned=0.0, effective=0.0): return {'value':{'remaining_hours': planned - effective}} def onchange_project(self, cr, uid, id, project_id): @@ -678,21 +713,21 @@ class task(base_stage, osv.osv): #FIXME why there is already the copy and the old one self.write(cr, uid, new, {'parent_ids':[(6,0,set(parent_ids))], 'child_ids':[(6,0, set(child_ids))]}) - def copy_data(self, cr, uid, id, default={}, context=None): + def copy_data(self, cr, uid, id, default=None, context=None): + if default is None: + default = {} default = default or {} default.update({'work_ids':[], 'date_start': False, 'date_end': False, 'date_deadline': False}) if not default.get('remaining_hours', False): default['remaining_hours'] = float(self.read(cr, uid, id, ['planned_hours'])['planned_hours']) default['active'] = True - default['stage_id'] = False if not default.get('name', False): default['name'] = self.browse(cr, uid, id, context=context).name or '' if not context.get('copy',False): - new_name = _("%s (copy)")%default.get('name','') + new_name = _("%s (copy)") % (default.get('name', '')) default.update({'name':new_name}) return super(task, self).copy_data(cr, uid, id, default, context) - def _is_template(self, cr, uid, ids, field_name, arg, context=None): res = {} for task in self.browse(cr, uid, ids, context=context): @@ -715,20 +750,20 @@ class task(base_stage, osv.osv): 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True), 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."), 'stage_id': fields.many2one('project.task.type', 'Stage', - domain="['|', ('project_ids', '=', project_id), ('case_default', '=', True)]"), + domain="['&', ('fold', '=', False), '|', ('project_ids', '=', project_id), ('case_default', '=', True)]"), 'state': fields.related('stage_id', 'state', type="selection", store=True, - selection=_TASK_STATE, string="State", readonly=True, - help='The state is set to \'Draft\', when a case is created.\ - If the case is in progress the state is set to \'Open\'.\ - When the case is over, the state is set to \'Done\'.\ - If the case needs to be reviewed then the state is \ + selection=_TASK_STATE, string="Status", readonly=True, + help='The status is set to \'Draft\', when a case is created.\ + If the case is in progress the status is set to \'Open\'.\ + When the case is over, the status is set to \'Done\'.\ + If the case needs to be reviewed then the status is \ set to \'Pending\'.'), - 'categ_ids': fields.many2many('project.category', string='Categories'), - 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', + 'categ_ids': fields.many2many('project.category', string='Tags'), + 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State', help="A task's kanban state indicates special situations affecting it:\n" " * Normal is the default situation\n" " * Blocked indicates something is preventing the progress of this task\n" - " * Ready To Pull indicates the task is ready to be pulled to the next stage", + " * Ready for next stage indicates the task is ready to be pulled to the next stage", readonly=True, required=False), 'create_date': fields.datetime('Create Date', readonly=True,select=True), 'date_start': fields.datetime('Starting Date',select=True), @@ -762,7 +797,7 @@ class task(base_stage, osv.osv): }), 'user_id': fields.many2one('res.users', 'Assigned to'), 'delegated_user_id': fields.related('child_ids', 'user_id', type='many2one', relation='res.users', string='Delegated To'), - 'partner_id': fields.many2one('res.partner', 'Contact'), + 'partner_id': fields.many2one('res.partner', 'Customer'), 'work_ids': fields.one2many('project.task.work', 'task_id', 'Work done'), 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'), 'company_id': fields.many2one('res.company', 'Company'), @@ -770,11 +805,9 @@ class task(base_stage, osv.osv): 'color': fields.integer('Color Index'), 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True), } - _defaults = { 'stage_id': _get_default_stage_id, 'project_id': _get_default_project_id, - 'state': 'draft', 'kanban_state': 'normal', 'priority': '2', 'progress': 0, @@ -783,7 +816,6 @@ class task(base_stage, osv.osv): 'user_id': lambda obj, cr, uid, context: uid, 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=c), } - _order = "priority, sequence, date_start, name, id" def set_priority(self, cr, uid, ids, priority, *args): @@ -791,6 +823,11 @@ class task(base_stage, osv.osv): """ return self.write(cr, uid, ids, {'priority' : priority}) + def set_very_high_priority(self, cr, uid, ids, *args): + """Set task priority to very high + """ + return self.set_priority(cr, uid, ids, '0') + def set_high_priority(self, cr, uid, ids, *args): """Set task priority to high """ @@ -846,9 +883,8 @@ class task(base_stage, osv.osv): (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']), (_check_dates, 'Error ! Task end-date must be greater then task start-date', ['date_start','date_end']) ] - # + # Override view according to the company definition - # def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): users_obj = self.pool.get('res.users') if context is None: context = {} @@ -880,9 +916,9 @@ class task(base_stage, osv.osv): res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours',tm) return res - # **************************************** + # ---------------------------------------- # Case management - # **************************************** + # ---------------------------------------- def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None): """ Override of the base.stage method @@ -1014,10 +1050,12 @@ class task(base_stage, osv.osv): new_attachment_ids.append(attachment.copy(cr, uid, attachment_id, default={'res_id': delegated_task_id}, context=context)) return new_attachment_ids - def do_delegate(self, cr, uid, ids, delegate_data={}, context=None): + def do_delegate(self, cr, uid, ids, delegate_data=None, context=None): """ Delegate Task to another users. """ + if delegate_data is None: + delegate_data = {} assert delegate_data['user_id'], _("Delegated User should be specified") delegated_tasks = {} for task in self.browse(cr, uid, ids, context=context): @@ -1027,7 +1065,6 @@ class task(base_stage, osv.osv): 'user_id': delegate_data['user_id'] and delegate_data['user_id'][0] or False, 'planned_hours': delegate_data['planned_hours'] or 0.0, 'parent_ids': [(6, 0, [task.id])], - 'state': 'draft', 'description': delegate_data['new_task_description'] or '', 'child_ids': [], 'work_ids': [] @@ -1092,12 +1129,36 @@ class task(base_stage, osv.osv): }, context=context) return True - def create(self, cr, uid, vals, context=None): - task_id = super(task, self).create(cr, uid, vals, context=context) + def _subscribe_project_followers_to_task(self, cr, uid, task_id, context=None): + """ TDE note: not the best way to do this, we could override _get_followers + of task, and perform a better mapping of subtypes than a mapping + based on names. + However we will keep this implementation, maybe to be refactored + in 7.1 of future versions. """ + # task followers are project followers, with matching subtypes task_record = self.browse(cr, uid, task_id, context=context) + subtype_obj = self.pool.get('mail.message.subtype') + follower_obj = self.pool.get('mail.followers') if task_record.project_id: - project_follower_ids = [follower.id for follower in task_record.project_id.message_follower_ids] - self.message_subscribe(cr, uid, [task_id], project_follower_ids, context=context) + # create mapping + task_subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', False), ('res_model', '=', self._name)], context=context) + task_subtypes = subtype_obj.browse(cr, uid, task_subtype_ids, context=context) + # fetch subscriptions + follower_ids = follower_obj.search(cr, uid, [('res_model', '=', 'project.project'), ('res_id', '=', task_record.project_id.id)], context=context) + # copy followers + for follower in follower_obj.browse(cr, uid, follower_ids, context=context): + if not follower.subtype_ids: + continue + project_subtype_names = [project_subtype.name for project_subtype in follower.subtype_ids] + task_subtype_ids = [task_subtype.id for task_subtype in task_subtypes if task_subtype.name in project_subtype_names] + self.message_subscribe(cr, uid, [task_id], [follower.partner_id.id], + subtype_ids=task_subtype_ids, context=context) + + def create(self, cr, uid, vals, context=None): + task_id = super(task, self).create(cr, uid, vals, context=context) + # subscribe project followers to the task + self._subscribe_project_followers_to_task(cr, uid, task_id, context=context) + self._store_history(cr, uid, [task_id], context=context) self.create_send_note(cr, uid, [task_id], context=context) return task_id @@ -1116,13 +1177,18 @@ class task(base_stage, osv.osv): #if new_stage not in stages: #raise osv.except_osv(_('Warning!'), _('Stage is not defined in the project.')) write_vals = vals_reset_kstate if t.stage_id != new_stage else vals - super(task,self).write(cr, uid, [t.id], write_vals, context=context) + super(task, self).write(cr, uid, [t.id], write_vals, context=context) self.stage_set_send_note(cr, uid, [t.id], new_stage, context=context) result = True else: - result = super(task,self).write(cr, uid, ids, vals, context=context) + result = super(task, self).write(cr, uid, ids, vals, context=context) if ('stage_id' in vals) or ('remaining_hours' in vals) or ('user_id' in vals) or ('state' in vals) or ('kanban_state' in vals): self._store_history(cr, uid, ids, context=context) + + # subscribe new project followers to the task + if vals.get('project_id'): + for id in ids: + self._subscribe_project_followers_to_task(cr, uid, id, context=context) return result def unlink(self, cr, uid, ids, context=None): @@ -1160,6 +1226,44 @@ class task(base_stage, osv.osv): return result # --------------------------------------------------- + # Mail gateway + # --------------------------------------------------- + + def message_new(self, cr, uid, msg, custom_values=None, context=None): + """ Override to updates the document according to the email. """ + if custom_values is None: custom_values = {} + custom_values.update({ + 'name': msg.get('subject'), + 'planned_hours': 0.0, + }) + return super(task,self).message_new(cr, uid, msg, custom_values=custom_values, context=context) + + def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): + """ Override to update the task according to the email. """ + if update_vals is None: update_vals = {} + act = False + maps = { + 'cost':'planned_hours', + } + for line in msg['body'].split('\n'): + line = line.strip() + res = tools.misc.command_re.match(line) + if res: + match = res.group(1).lower() + field = maps.get(match) + if field: + try: + update_vals[field] = float(res.group(2).lower()) + except (ValueError, TypeError): + pass + elif match.lower() == 'state' \ + and res.group(2).lower() in ['cancel','close','draft','open','pending']: + act = 'do_%s' % res.group(2).lower() + if act: + getattr(self,act)(cr, uid, ids, context=context) + return super(task,self).message_update(cr, uid, msg, update_vals=update_vals, context=context) + + # --------------------------------------------------- # OpenChatter methods and notifications # --------------------------------------------------- @@ -1187,21 +1291,31 @@ class task(base_stage, osv.osv): def stage_set_send_note(self, cr, uid, ids, stage_id, context=None): """ Override of the (void) default notification method. """ stage_name = self.pool.get('project.task.type').name_get(cr, uid, [stage_id], context=context)[0][1] - return self.message_post(cr, uid, ids, body= _("Stage changed to %s.") % (stage_name), subtype="stage change", context=context) + return self.message_post(cr, uid, ids, body=_("Stage changed to %s.") % (stage_name), + context=context) def create_send_note(self, cr, uid, ids, context=None): - return self.message_post(cr, uid, ids, body=_("Task has been created."), subtype="new", context=context) + return self.message_post(cr, uid, ids, body=_("Task has been created."), context=context) def case_draft_send_note(self, cr, uid, ids, context=None): - msg = _('Task has been set as draft.') - return self.message_post(cr, uid, ids, body=msg, context=context) + return self.message_post(cr, uid, ids, body=_('Task has been set as draft.'), context=context) def do_delegation_send_note(self, cr, uid, ids, context=None): for task in self.browse(cr, uid, ids, context=context): msg = _('Task has been delegated to %s.') % (task.user_id.name) self.message_post(cr, uid, [task.id], body=msg, context=context) return True - + + def project_task_reevaluate(self, cr, uid, ids, context=None): + if self.pool.get('res.users').has_group(cr, uid, 'project.group_time_work_estimation_tasks'): + return { + 'view_type': 'form', + "view_mode": 'form', + 'res_model': 'project.task.reevaluate', + 'type': 'ir.actions.act_window', + 'target': 'new', + } + return self.do_reopen(cr, uid, ids, context=context) class project_work(osv.osv): _name = "project.task.work" @@ -1246,7 +1360,7 @@ class account_analytic_account(osv.osv): _inherit = 'account.analytic.account' _description = 'Analytic Account' _columns = { - 'use_tasks': fields.boolean('Tasks',help="If check,this contract will be available in the project menu and you will be able to manage tasks or track issues"), + '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"), 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'), } @@ -1308,16 +1422,15 @@ class project_project(osv.osv): 'use_tasks': True } - -# -# Tasks History, used for cumulative flow charts (Lean/Agile) -# - class project_task_history(osv.osv): + """ + Tasks History, used for cumulative flow charts (Lean/Agile) + """ _name = 'project.task.history' _description = 'History of Tasks' _rec_name = 'task_id' _log_access = False + def _get_date(self, cr, uid, ids, name, arg, context=None): result = {} for history in self.browse(cr, uid, ids, context=context): @@ -1356,7 +1469,7 @@ class project_task_history(osv.osv): 'task_id': fields.many2one('project.task', 'Task', ondelete='cascade', required=True, select=True), 'type_id': fields.many2one('project.task.type', 'Stage'), 'state': fields.selection([('draft', 'New'), ('cancelled', 'Cancelled'),('open', 'In Progress'),('pending', 'Pending'), ('done', 'Done')], 'Status'), - 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready To Pull')], 'Kanban State', required=False), + 'kanban_state': fields.selection([('normal', 'Normal'),('blocked', 'Blocked'),('done', 'Ready for next stage')], 'Kanban State', required=False), 'date': fields.date('Date', select=True), 'end_date': fields.function(_get_date, string='End Date', type="date", store={ 'project.task.history': (_get_related_date, None, 20) @@ -1369,35 +1482,40 @@ class project_task_history(osv.osv): 'date': fields.date.context_today, } - class project_task_history_cumulative(osv.osv): _name = 'project.task.history.cumulative' _table = 'project_task_history_cumulative' _inherit = 'project.task.history' _auto = False + _columns = { 'end_date': fields.date('End Date'), - 'project_id': fields.related('task_id', 'project_id', string='Project', type='many2one', relation='project.project') + 'project_id': fields.many2one('project.project', 'Project'), } + def init(self, cr): - cr.execute(""" CREATE OR REPLACE VIEW project_task_history_cumulative AS ( + tools.drop_view_if_exists(cr, 'project_task_history_cumulative') + + cr.execute(""" CREATE VIEW project_task_history_cumulative AS ( SELECT - history.date::varchar||'-'||history.history_id::varchar as id, - history.date as end_date, + history.date::varchar||'-'||history.history_id::varchar AS id, + history.date AS end_date, * FROM ( SELECT - id as history_id, - date+generate_series(0, CAST((coalesce(end_date,DATE 'tomorrow')::date - date)AS integer)-1) as date, - task_id, type_id, user_id, kanban_state, state, - greatest(remaining_hours,1) as remaining_hours, greatest(planned_hours,1) as planned_hours + h.id AS history_id, + h.date+generate_series(0, CAST((coalesce(h.end_date, DATE 'tomorrow')::date - h.date) AS integer)-1) AS date, + h.task_id, h.type_id, h.user_id, h.kanban_state, h.state, + greatest(h.remaining_hours, 1) AS remaining_hours, greatest(h.planned_hours, 1) AS planned_hours, + t.project_id FROM - project_task_history - ) as history + project_task_history AS h + JOIN project_task AS t ON (h.task_id = t.id) + + ) AS history ) """) - class project_category(osv.osv): """ Category of project's task (or issue) """ _name = "project.category"