#
##############################################################################
-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
'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"""
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)])
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."),
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):
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'] = []
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
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,
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
# 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
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 self.message_post(cr, uid, ids, body=_("Project has been <b>created</b>."), context=context)
def set_open_send_note(self, cr, uid, ids, context=None):
- message = _("Project has been <b>opened</b>.")
- return self.message_post(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=_("Project has been <b>opened</b>."), context=context)
def set_pending_send_note(self, cr, uid, ids, context=None):
- message = _("Project is now <b>pending</b>.")
- return self.message_post(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=_("Project is now <b>pending</b>."), context=context)
def set_cancel_send_note(self, cr, uid, ids, context=None):
- message = _("Project has been <b>cancelled</b>.")
- return self.message_post(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=_("Project has been <b>canceled</b>."), context=context)
def set_close_send_note(self, cr, uid, ids, context=None):
- message = _("Project has been <b>closed</b>.")
- return self.message_post(cr, uid, ids, body=message, context=context)
+ return self.message_post(cr, uid, ids, body=_("Project has been <b>closed</b>."), context=context)
def write(self, cr, uid, ids, vals, context=None):
# if alias_model has been changed, update alias_model_id accordingly
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')
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,
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):
#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):
'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),
}),
'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'),
'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,
'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):
"""
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
"""
(_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 = {}
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
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):
'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': []
}, context=context)
return True
+ 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:
+ # 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
#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):
return result
# ---------------------------------------------------
- # mail gateway
+ # 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': subject,
+ 'name': msg.get('subject'),
'planned_hours': 0.0,
- 'subject': msg.get('subject'),
})
- return super(project_tasks,self).message_new(cr, uid, msg, custom_values=custom_values, context=context)
+ 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. """
act = 'do_%s' % res.group(2).lower()
if act:
getattr(self,act)(cr, uid, ids, context=context)
- return super(project_tasks,self).message_update(cr, uid, msg, update_vals=update_vals, context=context)
+ return super(task,self).message_update(cr, uid, msg, update_vals=update_vals, context=context)
# ---------------------------------------------------
# OpenChatter methods and notifications
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 <b>%s</b>.") % (stage_name), context=context)
+ return self.message_post(cr, uid, ids, body=_("Stage changed to <b>%s</b>.") % (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 <b>created</b>."), context=context)
def case_draft_send_note(self, cr, uid, ids, context=None):
- msg = _('Task has been set as <b>draft</b>.')
- return self.message_post(cr, uid, ids, body=msg, context=context)
+ return self.message_post(cr, uid, ids, body=_('Task has been set as <b>draft</b>.'), 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 <b>delegated</b> to <em>%s</em>.') % (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"
_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'),
}
'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):
'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)
'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"