diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml
index 1a9fb973655..c97e5e35471 100644
--- a/addons/project/project_view.xml
+++ b/addons/project/project_view.xml
@@ -283,14 +283,14 @@
-
-
+
+
-
+
diff --git a/addons/project_timesheet/project_timesheet.py b/addons/project_timesheet/project_timesheet.py
index 54a4a2a3b84..65f840275ca 100644
--- a/addons/project_timesheet/project_timesheet.py
+++ b/addons/project_timesheet/project_timesheet.py
@@ -13,6 +13,13 @@ from openerp.exceptions import UserError
class project_project(osv.osv):
_inherit = 'project.project'
+ _columns = {
+ 'subtask_project_id': fields.many2one(
+ 'project.project', string='Sub-task Project',
+ help="Choosing a sub-tasks project will both enable sub-tasks and set their default project (possibly the project itself)",
+ ondelete="restrict"),
+ }
+
def open_timesheets(self, cr, uid, ids, context=None):
""" open Timesheets view """
mod_obj = self.pool.get('ir.model.data')
@@ -49,28 +56,63 @@ class task(osv.osv):
# Compute: effective_hours, total_hours, progress
def _hours_get(self, cr, uid, ids, field_names, args, context=None):
+ def _get_child_hour(child, res):
+ if child.stage_id and child.stage_id.fold:
+ return res[child.id]['effective_hours'] + res[child.id]['children_hours']
+ else:
+ return max(child.planned_hours, res[child.id]['effective_hours'] + res[child.id]['children_hours'])
+
res = {}
- for task in self.browse(cr, uid, ids, context=context):
- res[task.id] = {'effective_hours': 0.0, 'remaining_hours': task.planned_hours, 'progress': 0.0, 'total_hours': task.planned_hours, 'delay_hours': 0.0}
- tasks_data = self.pool['account.analytic.line'].read_group(cr, uid, [('task_id', 'in', ids)], ['task_id','unit_amount'], ['task_id'], context=context)
- for data in tasks_data:
- task = self.browse(cr, uid, data['task_id'][0], context=context)
- res[data['task_id'][0]] = {'effective_hours': data.get('unit_amount', 0.0), 'remaining_hours': task.planned_hours - data.get('unit_amount', 0.0)}
- res[data['task_id'][0]]['total_hours'] = res[data['task_id'][0]]['remaining_hours'] + data.get('unit_amount', 0.0)
- res[data['task_id'][0]]['delay_hours'] = res[data['task_id'][0]]['total_hours'] - task.planned_hours
- res[data['task_id'][0]]['progress'] = 0.0
- if (task.planned_hours > 0.0 and data.get('unit_amount', 0.0)):
- res[data['task_id'][0]]['progress'] = round(min(100.0 * data.get('unit_amount', 0.0) / task.planned_hours, 99.99),2)
+ task_to_read = self.search(cr, uid, [('id', 'child_of', ids)])
+ task_to_sort = {}
+ for task in self.browse(cr, uid, task_to_read, context=context):
+ res[task.id] = {
+ 'effective_hours': 0.0,
+ 'remaining_hours': task.planned_hours,
+ 'progress': 0.0,
+ 'total_hours': task.planned_hours,
+ 'delay_hours': 0.0,
+ 'children_hours' : 0.0
+ }
+ task_to_sort[task.id] = [child.id for child in task.child_ids]
+
+ tasks_data = self.pool['account.analytic.line'].read_group(
+ cr, uid, [('task_id', 'in', task_to_read)], ['task_id','unit_amount'], ['task_id'], context=context)
+ tasks_data_dict = {data['task_id'][0] : data for data in tasks_data}
+ for task_id in tools.topological_sort(task_to_sort):
+ task = self.browse(cr, uid, task_id, context=context)
+ values = {
+ 'effective_hours': tasks_data_dict.get(task_id, {}).get('unit_amount', 0.0),
+ 'children_hours' : sum([_get_child_hour(child, res) for child in task.child_ids])
+ }
+ values['remaining_hours'] = task.planned_hours - values['effective_hours'] - values['children_hours']
+ values['total_hours'] = values['remaining_hours'] + values['effective_hours']
+ values['delay_hours'] = values['total_hours'] - task.planned_hours
+ values['progress'] = 0.0
# TDE CHECK: if task.state in ('done','cancelled'):
if task.stage_id and task.stage_id.fold:
- res[data['task_id'][0]]['progress'] = 100.0
+ values['progress'] = 100.0
+ elif task.planned_hours > 0.0:
+ values['progress'] = round(min(100.0 * (values['effective_hours'] + values['children_hours']) / task.planned_hours, 99.99),2)
+ res[task.id] = values
+ return {k : val for k, val in res.iteritems() if k in ids} #Filter only needed result
+
+ def _get_subtask_count(self, cr, uid, ids, field_names, args, context=None):
+ res = dict.fromkeys(ids, 0)
+ for task_id in ids:
+ res[task_id] = (self.search_count(
+ cr, uid, [('id', 'child_of', task_id)], context=context) - 1) # don't count task itself
return res
- def _get_task(self, cr, uid, id, context=None):
+ def _get_task(self, cr, uid, ids, context=None):
res = []
- for line in self.pool.get('account.analytic.line').search_read(cr,uid,[('task_id', '!=', False),('id','in',id)], context=context):
+ for line in self.pool['account.analytic.line'].search_read(
+ cr, uid, [('task_id', '!=', False), ('id', 'in', ids)], context=context):
res.append(line['task_id'][0])
- return res
+ return self.pool['project.task'].search(cr, uid, [('id', 'parent_of', res)])
+
+ def _get_parent_tasks(self, cr, uid, ids, context=None):
+ return self.pool['project.task'].search(cr, uid, [('id', 'parent_of', ids)])
def _get_total_hours(self):
return super(task, self)._get_total_hours() + self.effective_hours
@@ -78,38 +120,50 @@ class task(osv.osv):
_columns = {
'remaining_hours': fields.function(_hours_get, string='Remaining Hours', multi='line_id', help="Total remaining time, can be re-estimated periodically by the assignee of the task.",
store = {
- 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10),
+ 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10),
+ 'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10),
+ }),
+ 'children_hours': fields.function(_hours_get, string='Sub-tasks Hours', type='float', multi='line_id',
+ help="Sum of the planned hours of all sub-tasks (when a sub-task is closed or its spent hours exceed its planned hours, spent hours are counted instead)",
+ store = {
+ 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10),
'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10),
}),
'effective_hours': fields.function(_hours_get, string='Hours Spent', multi='line_id', help="Computed using the sum of the task work done.",
store = {
- 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10),
+ 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10),
'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10),
}),
'total_hours': fields.function(_hours_get, string='Total', multi='line_id', help="Computed as: Time Spent + Remaining Time.",
store = {
- 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10),
+ 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'stage_id'], 10),
'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10),
}),
'progress': fields.function(_hours_get, string='Working Time Progress (%)', multi='line_id', 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",
store = {
- 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'state', 'stage_id'], 10),
+ 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours', 'parent_id', 'state', 'stage_id'], 10),
'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10),
}),
'delay_hours': fields.function(_hours_get, string='Delay Hours', multi='line_id', help="Computed as difference between planned hours by the project manager and the total hours of the task.",
store = {
- 'project.task': (lambda self, cr, uid, ids, c={}: ids, ['timesheet_ids', 'remaining_hours', 'planned_hours'], 10),
+ 'project.task': (_get_parent_tasks, ['timesheet_ids', 'remaining_hours', 'planned_hours','parent_id', 'stage_id'], 10),
'account.analytic.line': (_get_task, ['task_id', 'unit_amount'], 10),
- }),
+ }),
'timesheet_ids': fields.one2many('account.analytic.line', 'task_id', 'Timesheets'),
'analytic_account_id': fields.related('project_id', 'analytic_account_id',
type='many2one', relation='account.analytic.account', string='Analytic Account', store=True),
+ 'parent_id' : fields.many2one('project.task', string='Parent Task', select=True),
+ 'child_ids' : fields.one2many('project.task', 'parent_id', string="Sub-tasks"),
+ 'subtask_project_id': fields.related('project_id', 'subtask_project_id', type='many2one', relation='project.project', string='Sub-task Project'),
+ 'subtask_count' : fields.function(_get_subtask_count, type='integer', string="Sub-task count"),
}
_defaults = {
'progress': 0,
}
+ _constraints = [(osv.osv._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]
+
def _prepare_delegate_values(self, cr, uid, ids, delegate_data, context=None):
vals = super(task, self)._prepare_delegate_values(cr, uid, ids, delegate_data, context)
for task in self.browse(cr, uid, ids, context=context):
@@ -125,7 +179,6 @@ class task(osv.osv):
project = self.pool['project.project'].browse(cr, uid, project_id, context=context)
return result
-
class res_partner(osv.osv):
_inherit = 'res.partner'
diff --git a/addons/project_timesheet/project_timesheet_view.xml b/addons/project_timesheet/project_timesheet_view.xml
index d3f1ffaed30..c8d9ef4ce84 100644
--- a/addons/project_timesheet/project_timesheet_view.xml
+++ b/addons/project_timesheet/project_timesheet_view.xml
@@ -10,6 +10,9 @@
+
+
+
@@ -50,11 +53,34 @@
+
+ Sub-tasks
+ project.task
+ kanban,tree,form,calendar,pivot,graph
+ [('id', 'child_of', active_id), ('id', '!=', active_id)]
+
+ {
+ 'default_parent_id' : active_id,
+ 'default_project_id' : project_id,
+ 'default_name' : name + ':',
+ 'default_partner_id' : partner_id,
+ }
+
+
+
+
project.task.form.inherited
project.task
+
onchange_project(project_id)
@@ -63,6 +89,11 @@
+
+
+
+
+
@@ -77,8 +108,9 @@
@@ -113,7 +145,26 @@
Timesheets
+
+
+
+ project.task.timesheet.kanban.inherited.progress
+ project.task
+
+
+
+
+
+
+
diff --git a/addons/sale_service/models/sale_service.py b/addons/sale_service/models/sale_service.py
index bcaabaaf20e..e6343e940dc 100644
--- a/addons/sale_service/models/sale_service.py
+++ b/addons/sale_service/models/sale_service.py
@@ -118,6 +118,17 @@ class project_task(osv.osv):
"context": {"create": False, "show_sale": True},
}
+ def onchange_parent_id(self, cr, uid, ids, parent_id, context=None):
+ if not parent_id:
+ return {'value' : {'procurement_id': False, 'sale_line_id': False }}
+ parent_task = self.browse(cr, uid, parent_id, context=context)
+ return {
+ 'value' : {
+ 'procurement_id' : parent_task.procurement_id.id,
+ 'sale_line_id' : parent_task.sale_line_id.id,
+ }
+ }
+
class ProductProduct(models.Model):
_inherit = 'product.product'
diff --git a/addons/sale_service/views/sale_service_view.xml b/addons/sale_service/views/sale_service_view.xml
index e097b02d92b..3a49020d835 100644
--- a/addons/sale_service/views/sale_service_view.xml
+++ b/addons/sale_service/views/sale_service_view.xml
@@ -37,8 +37,12 @@
string="Sales Order"/>
+
+
+ onchange_parent_id(parent_id)
+
diff --git a/addons/web_kanban/static/src/less/kanban_view.less b/addons/web_kanban/static/src/less/kanban_view.less
index fbbcc92365f..ddd7f6374d1 100644
--- a/addons/web_kanban/static/src/less/kanban_view.less
+++ b/addons/web_kanban/static/src/less/kanban_view.less
@@ -96,6 +96,10 @@
.oe_kanban_bottom_left {
float: left;
+ .oe_kanban_align {
+ display: inline-block;
+ vertical-align: middle;
+ }
}
.o_kanban_tags {