[ADD] project_timesheet, sale_service: support for project sub-tasks
This commit introduces "Sub-task", a concept remotely similar to the "delegated tasks" available in version 7.0, but covering a more specific and narrow scope. Activating Sub-tasks -------------------- Sub-tasks are made available within any project by configuring a "sub-task project" in the project settings. This feature is considered low-level and technical, and only accessible with "Technical Features" enabled. The "sub-task project" may be the project itself. Using Sub-tasks --------------- Conceptually, a sub-task represents the allocation of a part of the scope of its parent task. Multiple sub-task levels are possible. Once activated in project settings, a new "Sub-tasks" smart button will appear on the task form, giving the option to view and create sub-tasks. In addition, a new field "Sub-tasks Hours" will appear next to the other computed "Hours" fields, representing the total hours allocated to sub-tasks. (Technically, the sum of the maximum between the planned hours and spent hours of all sub-tasks, with spent hours always used when a task is closed) These sub-task hours will be dynamically subtracted from the remaining hours of the task, and will therefore affect the task's progress as well. Upon creation, a sub-task will inherit the link to the original sales order and procurement from its parent task, and will by default be assigned to the "sub-task project" set on the project of the parent task. Enhancement ----------- The kanban view for tasks will now display a color-coded label with the number of remaining hours, whenever the percentage of remaining hours is below 20% (orange=20%-, red=1%-).
This commit is contained in:
committed by
Olivier Dony
parent
9523d59fe3
commit
9eac32beea
@@ -283,14 +283,14 @@
|
||||
<div class="oe_clear"/>
|
||||
</page>
|
||||
<page string="Extra Info">
|
||||
<group col="4">
|
||||
<group col="2">
|
||||
<group>
|
||||
<group>
|
||||
<field name="sequence" groups="base.group_no_one"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
<field name="displayed_image_id" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<group col="2">
|
||||
<group>
|
||||
<field name="date_assign" groups="base.group_no_one"/>
|
||||
<field name="date_last_stage_update" groups="base.group_no_one"/>
|
||||
</group>
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" name="open_timesheets" type="object" icon="fa-calendar" string="Timesheets"/>
|
||||
</div>
|
||||
<field name="user_id" position="after">
|
||||
<field name="subtask_project_id" groups="base.group_no_one" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -50,11 +53,34 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_project_task_sub_task" model="ir.actions.act_window">
|
||||
<field name="name">Sub-tasks</field>
|
||||
<field name="res_model">project.task</field>
|
||||
<field name="view_mode">kanban,tree,form,calendar,pivot,graph</field>
|
||||
<field name="domain">[('id', 'child_of', active_id), ('id', '!=', active_id)]</field>
|
||||
<field name="context">
|
||||
{
|
||||
'default_parent_id' : active_id,
|
||||
'default_project_id' : project_id,
|
||||
'default_name' : name + ':',
|
||||
'default_partner_id' : partner_id,
|
||||
}
|
||||
</field>
|
||||
<field name="search_view_id" ref="project.view_task_search_form"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_task_form2_inherited">
|
||||
<field name="name">project.task.form.inherited</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2" />
|
||||
<field name="arch" type="xml">
|
||||
<button name="toggle_active" position="after">
|
||||
<button name="%(act_project_task_sub_task)d" type="action" class="oe_stat_button" icon="fa-tasks"
|
||||
attrs="{'invisible' : [('subtask_project_id', '=', False)]}"
|
||||
context="{'project_id': subtask_project_id, 'name': name, 'partner_id': partner_id}" >
|
||||
<field string="Sub-tasks" name="subtask_count" widget="statinfo"/>
|
||||
</button>
|
||||
</button>
|
||||
<field name="project_id" position="attributes">
|
||||
<attribute name="on_change">onchange_project(project_id)</attribute>
|
||||
</field>
|
||||
@@ -63,6 +89,11 @@
|
||||
<field name="progress" widget="progressbar"
|
||||
groups="project.group_time_work_estimation_tasks"/>
|
||||
</field>
|
||||
<field name="partner_id" position="after">
|
||||
<field name="parent_id" attrs="{'invisible' : [('parent_id', '=', False)]}" />
|
||||
<field name="child_ids" invisible="1" />
|
||||
<field name="subtask_project_id" invisible="1" />
|
||||
</field>
|
||||
<xpath expr="//notebook/page[@name='description_page']" position="after">
|
||||
<page string="Timesheets" groups="project.group_tasks_work_on_tasks,project.group_time_work_estimation_tasks">
|
||||
<field name="timesheet_ids" groups="project.group_tasks_work_on_tasks" context="{'default_account_id' : analytic_account_id, 'default_is_timesheet' : 1}">
|
||||
@@ -77,8 +108,9 @@
|
||||
</field>
|
||||
<group>
|
||||
<group class="oe_subtotal_footer oe_right" name="project_hours" groups="project.group_time_work_estimation_tasks">
|
||||
<field name="effective_hours" widget="float_time" groups="project.group_time_work_estimation_tasks"/>
|
||||
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" groups="project.group_time_work_estimation_tasks"/>
|
||||
<field name="effective_hours" widget="float_time" />
|
||||
<field name="children_hours" widget="float_time" attrs="{'invisible' : [('children_hours', '=', 0.0)]}" />
|
||||
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
@@ -113,7 +145,26 @@
|
||||
<a name="open_timesheets" type="object">Timesheets</a>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_task_kanban_inherited_progress" model="ir.ui.view">
|
||||
<field name="name">project.task.timesheet.kanban.inherited.progress</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<templates position="before">
|
||||
<field name="progress" />
|
||||
<field name="remaining_hours" />
|
||||
</templates>
|
||||
<div class="oe_kanban_bottom_left" position="inside">
|
||||
<t t-if="record.progress.raw_value > 80 and record.progress.raw_value < 100">
|
||||
<div t-att-class="'oe_kanban_align label label-' + (record.progress.raw_value >= 99 ? 'danger': 'warning')"
|
||||
groups="project.group_time_work_estimation_tasks" title="Remaining hours">
|
||||
<field name="remaining_hours" widget="float_time" />
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -37,8 +37,12 @@
|
||||
string="Sales Order"/>
|
||||
</xpath>
|
||||
<field name="company_id" position="before">
|
||||
<field name="procurement_id" groups="base.group_no_one"/>
|
||||
<field name="sale_line_id" string="Order Line" groups="base.group_no_one"/>
|
||||
</field>
|
||||
<field name="parent_id" position="attributes">
|
||||
<attribute name="on_change">onchange_parent_id(parent_id)</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_order_form_inherit_sale_service" model="ir.ui.view">
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
|
||||
.oe_kanban_bottom_left {
|
||||
float: left;
|
||||
.oe_kanban_align {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_tags {
|
||||
|
||||
Reference in New Issue
Block a user