[IMP] project, hr_timesheet, *: improve generic ux

Purpose of this commit to improve generic UX for the project app.

So, In this commit done following changes:

- add 'subtask_effective_hours' and 'total_hours_spent' fields in the page of
subtask in task form view.
- change invisible attrs to column_invisible attrs for block_by page in task
form view.
- add 'task in progress' subtype for project.task and move 'stage changed'
subtype above 'task blocked'.
- remove the 'is_close' field and all its references from project.task.type.
- unarchive all of its tasks when the user unarchives the task stage.
- sort projects on 'is_favorite'.
- internal users following the project automatically follow its updates and
their notification preferences propagated accordingly.
- duplicating a task will duplicate all of its subtasks.
- move parent task field in extra info page of task form view.
- change the label of 'edit' to 'view' in the burger menu of project kanban view.
- user can delete task without sale_order validation error.
- add constraints for 'seller_ids' field.
- add remaining_hours_so field in task tree view.
- add unusual_days in task calendar view.
- add 'no content helper' in action of burndown chart.

task-2536044

closes odoo/odoo#74018

Related: odoo/enterprise#19880
Related: odoo/upgrade#2801
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
Co-authored-by: Kartik Chavda <kcv@odoo.com>
This commit is contained in:
Bonis Modi
2021-11-23 12:04:00 +00:00
committed by Laurent Stukkens (LTU)
co-authored by Kartik Chavda
parent 6b9183a322
commit f4c054e473
17 changed files with 183 additions and 70 deletions
+6 -12
View File
@@ -1,18 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from pytz import UTC
from datetime import datetime, time
from dateutil.rrule import rrule, DAILY
from random import choice
from string import digits
from werkzeug.urls import url_encode
from dateutil.relativedelta import relativedelta
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.osv.query import Query
from odoo.exceptions import ValidationError, AccessError, UserError
from odoo.exceptions import ValidationError, AccessError
from odoo.osv import expression
from odoo.tools.misc import format_date
@@ -438,14 +436,10 @@ class HrEmployeePrivate(models.Model):
if not self:
return {}
self.ensure_one()
calendar = self.resource_calendar_id
if not calendar:
return {}
dfrom = datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=pytz.UTC)
dto = datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=pytz.UTC)
works = {d[0].date() for d in calendar._work_intervals_batch(dfrom, dto)[False]}
return {fields.Date.to_string(day.date()): (day.date() not in works) for day in rrule(DAILY, dfrom, until=dto)}
return self.resource_calendar_id._get_unusual_days(
datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=UTC),
datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=UTC)
)
# ---------------------------------------------------------
# Messaging
@@ -112,6 +112,8 @@
<field name="planned_hours" widget="timesheet_uom_no_toggle" sum="Initially Planned Hours" optional="hide"/>
<field name="effective_hours" widget="timesheet_uom" sum="Effective Hours" optional="hide"/>
<field name="remaining_hours" widget="timesheet_uom" sum="Remaining Hours" optional="hide" decoration-danger="progress &gt;= 100" decoration-warning="progress &gt;= 80 and progress &lt; 100"/>
<field name="subtask_effective_hours" string="Sub-tasks Hours Spent" widget="timesheet_uom" sum="Sub-tasks Hours Spent" optional="hide"/>
<field name="total_hours_spent" string="Total Hours" widget="timesheet_uom" sum="Total Hours" optional="hide"/>
<field name="progress" widget="progressbar" optional="hide"/>
</xpath>
<xpath expr="//notebook/page[@name='description_page']" position="after">
+1
View File
@@ -72,6 +72,7 @@
'project/static/src/js/project_rating_graph_view.js',
'project/static/src/js/project_rating_pivot_view.js',
'project/static/src/js/project_task_kanban_examples.js',
'project/static/src/js/subtasks_list_renderer.js',
'project/static/src/js/tours/project.js',
'project/static/src/js/project_calendar.js',
'project/static/src/js/right_panel/*',
+25 -3
View File
@@ -8,6 +8,12 @@
<field name="hidden" eval="True"/>
<field name="description">Task Created</field>
</record>
<record id="mt_task_stage" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<field name="res_model">project.task</field>
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<record id="mt_task_blocked" model="mail.message.subtype">
<field name="name">Task Blocked</field>
<field name="res_model">project.task</field>
@@ -20,11 +26,10 @@
<field name="default" eval="False"/>
<field name="description">Task ready for Next Stage</field>
</record>
<record id="mt_task_stage" model="mail.message.subtype">
<field name="name">Stage Changed</field>
<record id="mt_task_progress" model="mail.message.subtype">
<field name="name">Task in Progress</field>
<field name="res_model">project.task</field>
<field name="default" eval="False"/>
<field name="description">Stage changed</field>
</record>
<record id="mt_task_rating" model="mail.message.subtype">
<field name="name">Task Rating</field>
@@ -37,6 +42,14 @@
<field name="default" eval="False"/>
<field name="hidden" eval="True"/>
</record>
<!-- Update-related subtypes for messaging / Chatter -->
<record id="mt_update_create" model="mail.message.subtype">
<field name="name">Update Created</field>
<field name="res_model">project.update</field>
<field name="default" eval="False"/>
<field name="description">Update Created</field>
<field name="hidden" eval="True"/>
</record>
<!-- Project-related subtypes for messaging / Chatter -->
<record id="mt_project_stage_change" model="mail.message.subtype">
<field name="name">Project Stage Changed</field>
@@ -94,4 +107,13 @@
<field name="relation_field">project_id</field>
<field name="hidden" eval="True"/>
</record>
<record id="mt_project_update_create" model="mail.message.subtype">
<field name="name">Update Created</field>
<field name="sequence">16</field>
<field name="res_model">project.project</field>
<field name="default" eval="False"/>
<field name="parent_id" ref="mt_update_create"/>
<field name="relation_field">project_id</field>
<field name="hidden" eval="True"/>
</record>
</odoo>
-2
View File
@@ -64,14 +64,12 @@
<field name="sequence">20</field>
<field name="name">Done</field>
<field name="fold" eval="True"/>
<field name="is_closed" eval="True"/>
</record>
<record id="project_stage_3" model="project.task.type">
<field name="sequence">30</field>
<field name="name">Cancelled</field>
<field name="legend_done">Ready to reopen</field>
<field name="fold" eval="True"/>
<field name="is_closed" eval="True"/>
</record>
<record id="project_project_1" model="project.project">
+24 -15
View File
@@ -3,8 +3,9 @@
import ast
import json
from pytz import UTC
from collections import defaultdict
from datetime import timedelta, datetime
from datetime import timedelta, datetime, time
from random import randint
from odoo import api, Command, fields, models, tools, SUPERUSER_ID, _
@@ -99,7 +100,6 @@ class ProjectTaskType(models.Model):
help="Automatically modify the kanban state when the customer replies to the feedback for this stage.\n"
" * Good feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n"
" * Neutral or bad feedback will set the kanban state to 'blocked' (red bullet).\n")
is_closed = fields.Boolean('Closing Stage', help="Tasks in this stage are considered as closed.")
disabled_rating_warning = fields.Text(compute='_compute_disabled_rating_warning')
user_id = fields.Many2one('res.users', 'Stage Owner', index=True)
@@ -548,6 +548,7 @@ class Project(models.Model):
partners = set(task.message_partner_ids.ids) & set(partner_ids)
if partners:
task.message_subscribe(partner_ids=list(partners), subtype_ids=task_subtypes)
self.update_ids.message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
return res
def _alias_get_creation_values(self):
@@ -656,10 +657,6 @@ class Project(models.Model):
'context': {'search_default_group_date': 1, 'default_account_id': self.analytic_account_id.id}
}
def action_view_kanban_project(self):
# [XBO] TODO: remove me in master
return
# ---------------------------------------------
# PROJECT UPDATES
# ---------------------------------------------
@@ -857,7 +854,7 @@ class Task(models.Model):
project_id = self.env.context.get('default_project_id')
if not project_id:
return False
return self.stage_find(project_id, [('fold', '=', False), ('is_closed', '=', False)])
return self.stage_find(project_id, [('fold', '=', False)])
@api.model
def _default_company_id(self):
@@ -962,7 +959,7 @@ class Task(models.Model):
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
is_closed = fields.Boolean(related="stage_id.is_closed", string="Closing Stage", readonly=True, related_sudo=False)
is_closed = fields.Boolean(related="stage_id.fold", string="Closing Stage", related_sudo=False, help="Folded in Kanban stages are closing stages.")
parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
child_text = fields.Char(compute="_compute_child_text")
@@ -1389,8 +1386,7 @@ class Task(models.Model):
for task in self:
if task.project_id:
if task.project_id not in task.stage_id.project_ids:
task.stage_id = task.stage_find(task.project_id.id, [
('fold', '=', False), ('is_closed', '=', False)])
task.stage_id = task.stage_find(task.project_id.id, [('fold', '=', False)])
else:
task.stage_id = False
@@ -1431,6 +1427,8 @@ class Task(models.Model):
default['name'] = _("%s (copy)", self.name)
if self.recurrence_id:
default['recurrence_id'] = self.recurrence_id.copy().id
if self.allow_subtasks:
default['child_ids'] = [child.copy().id for child in self.child_ids]
return super(Task, self).copy(default)
@api.model
@@ -1777,7 +1775,7 @@ class Task(models.Model):
def update_date_end(self, stage_id):
project_task_type = self.env['project.task.type'].browse(stage_id)
if project_task_type.fold or project_task_type.is_closed:
if project_task_type.fold:
return {'date_end': fields.Datetime.now()}
return {'date_end': False}
@@ -1910,10 +1908,13 @@ class Task(models.Model):
def _track_subtype(self, init_values):
self.ensure_one()
if 'kanban_state_label' in init_values and self.kanban_state == 'blocked':
return self.env.ref('project.mt_task_blocked')
elif 'kanban_state_label' in init_values and self.kanban_state == 'done':
return self.env.ref('project.mt_task_ready')
mail_message_subtype_per_kanban_state = {
'blocked': 'project.mt_task_blocked',
'done': 'project.mt_task_ready',
'normal': 'project.mt_task_progress',
}
if 'kanban_state_label' in init_values and self.kanban_state in mail_message_subtype_per_kanban_state:
return self.env.ref(mail_message_subtype_per_kanban_state[self.kanban_state])
elif 'stage_id' in init_values:
return self.env.ref('project.mt_task_stage')
return super(Task, self)._track_subtype(init_values)
@@ -2171,6 +2172,14 @@ class Task(models.Model):
self.ensure_one()
return self.analytic_account_id or self.project_analytic_account_id
@api.model
def get_unusual_days(self, date_from, date_to=None):
calendar = self.env.company.resource_calendar_id
return calendar._get_unusual_days(
datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=UTC),
datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=UTC)
)
class ProjectTags(models.Model):
""" Tags of project's tasks """
_name = "project.tags"
@@ -63,7 +63,7 @@
<separator/>
<filter string="Unassigned Tasks" name="unassigned" domain="[('user_ids', '=', False)]"/>
<separator/>
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False), ('stage_id.is_closed', '=', False)]"/>
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False)]"/>
<separator/>
<filter name="filter_date_deadline" date="date_deadline"/>
<filter name="filter_date_assign" date="date_assign"/>
@@ -16,7 +16,7 @@
<filter name="filter_date_deadline" date="date_deadline"/>
<filter name="filter_date_assign" date="date_assign"/>
<filter string="Last Month" invisible="1" name="last_month" domain="[('date','&gt;=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False), ('stage_id.is_closed', '=', False)]"/>
<filter string="Open tasks" name="open_tasks" domain="[('stage_id.fold', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Date" name="date" context="{'group_by': 'date'}" />
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}" />
@@ -56,7 +56,11 @@
<field name="search_view_id" ref="project_task_burndown_chart_report_view_search"/>
<field name="context">{'search_default_project_id': active_id}</field>
<field name="domain">[('display_project_id', '!=', False)]</field>
<field name="help">
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No data yet!
</p>
<p>Analyze how quickly your team is completing your project's tasks and check if everything is going according to plan.</p>
</field>
</record>
@@ -0,0 +1,34 @@
/** @odoo-module **/
import ListRenderer from 'web.ListRenderer';
import Dialog from 'web.Dialog';
import { _t } from 'web.core';
export const SubTasksListRenderer = ListRenderer.extend({
events: Object.assign({}, ListRenderer.prototype.events, {
'click tr .o_list_record_remove': '_onClickOpenDialog',
}),
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Confirmation dialog is opened when deleted any record from list.
*
* @private
* @param {MouseEvent} ev
*/
_onClickOpenDialog(ev) {
ev.stopPropagation();
Dialog.confirm(this, _t("Are you sure you want to delete this record ?"), {
confirm_callback: () => {
this._onRemoveIconClick(ev);
},
cancel_callback: () => {
return false;
},
});
},
});
@@ -0,0 +1,21 @@
/** @odoo-module **/
import { FieldOne2Many } from 'web.relational_fields';
import fieldRegistry from 'web.field_registry';
import { SubTasksListRenderer } from '../subtasks_list_renderer';
const SubTasksFieldOne2Many = FieldOne2Many.extend({
/**
* We want to use our custom renderer for the list.
*
* @override
*/
_getRenderer() {
if (this.view.arch.tag === 'tree') {
return SubTasksListRenderer;
}
return this._super.apply(...arguments);
},
});
fieldRegistry.add('subtasks_one2many', SubTasksFieldOne2Many);
+13 -14
View File
@@ -30,6 +30,8 @@
<separator/>
<filter string="Late Tasks" name="late" domain="[('date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
<separator/>
<filter string="Open Tasks" name="open_tasks" domain="[('is_closed', '=', False)]"/>
<separator/>
<filter string="Rated Tasks" name="rating_task" domain="[('rating_last_value', '!=', 0.0)]" groups="project.group_project_rating"/>
<separator/>
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
@@ -127,7 +129,6 @@
</group>
<group>
<field name="fold"/>
<field name="is_closed" groups="base.group_no_one"/>
<field name="project_ids" widget="many2many_tags" options="{'color_field': 'color'}" groups="base.group_no_one"/>
</group>
</group>
@@ -691,14 +692,14 @@
</div>
</div>
</div>
<div class="o_kanban_card_manage_settings row" groups="project.group_project_manager">
<div role="menuitem" aria-haspopup="true" class="col-8">
<div class="o_kanban_card_manage_settings row">
<div role="menuitem" aria-haspopup="true" class="col-8" groups="project.group_project_manager">
<ul class="oe_kanban_colorpicker" data-field="color" role="popup"/>
</div>
<div role="menuitem" class="col-4">
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_wizard_action)d" type="action">Share</a>
<!-- [XBO] TODO: remove the name attribute in this a tag in master -->
<a class="dropdown-item" role="menuitem" type="edit" name="action_view_kanban_project">Edit</a>
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_wizard_action)d" type="action" groups="project.group_project_manager">Share</a>
<a class="dropdown-item" role="menuitem" type="edit" groups="project.group_project_manager">Edit</a>
<a class="dropdown-item" role="menuitem" type="open" groups="!project.group_project_manager">View</a>
</div>
</div>
</div>
@@ -888,11 +889,6 @@
options="{'no_open': True}"
widget="many2many_avatar_user"
domain="[('share', '=', False)]"/>
<field name="parent_id"
domain="[('parent_id', '=', False)]"
attrs="{'invisible' : [('allow_subtasks', '=', False)]}"
groups="base.group_no_one"
/>
</group>
<group>
<field name="active" invisible="1"/>
@@ -911,7 +907,7 @@
<field name="description" type="html" options="{'collaborative': true}"/>
</page>
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id, 'default_partner_id': partner_id}">
<field name="child_ids" context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id, 'default_partner_id': partner_id}" widget="subtasks_one2many">
<tree editable="bottom">
<field name="project_id" invisible="1"/>
<field name="is_closed" invisible="1"/>
@@ -989,6 +985,7 @@
<group>
<group>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" context="{'default_partner_id': partner_id}"/>
<field name="parent_id" domain="[('parent_id', '=', False)]" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags"/>
<field name="sequence" groups="base.group_no_one"/>
<field name="email_from" invisible="1"/>
@@ -1207,8 +1204,10 @@
<field name="model">project.task</field>
<field eval="2" name="priority"/>
<field name="arch" type="xml">
<calendar date_start="date_deadline" string="Tasks" mode="month" color="user_ids" event_limit="5"
hide_time="true" js_class="project_calendar" event_open_popup="true" quick_add="false">
<calendar date_start="date_deadline" string="Tasks" mode="month"
color="user_ids" event_limit="5" hide_time="true"
event_open_popup="true" quick_add="false" show_unusual_days="True"
js_class="project_calendar">
<field name="project_id" widget="project_private_task" filters="1"/>
<field name="user_ids" widget="many2many_avatar_user"/>
<field name="partner_id" attrs="{'invisible': [('partner_id', '=', False)]}"/>
+12
View File
@@ -636,6 +636,18 @@ class ResourceCalendar(models.Model):
)
return interval_dt(work_intervals[0]) if work_intervals else None
def _get_unusual_days(self, start_dt, end_dt):
if not self:
return {}
self.ensure_one()
if not start_dt.tzinfo:
start_dt = start_dt.replace(tzinfo=utc)
if not end_dt.tzinfo:
end_dt = end_dt.replace(tzinfo=utc)
works = {d[0].date() for d in self._work_intervals_batch(start_dt, end_dt)[False]}
return {fields.Date.to_string(day.date()): (day.date() not in works) for day in rrule(DAILY, start_dt, until=end_dt)}
# --------------------------------------------------
# External API
# --------------------------------------------------
-5
View File
@@ -169,11 +169,6 @@ class ProjectTask(models.Model):
product_id=task.sale_line_id.product_id.display_name,
))
@api.ondelete(at_uninstall=False)
def _unlink_except_linked_so(self):
if any(task.sale_line_id for task in self):
raise ValidationError(_('You have to unlink the task from the sale order item in order to delete it.'))
# ---------------------------------------------------
# Actions
# ---------------------------------------------------
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.exceptions import ValidationError
class ProductTemplate(models.Model):
@@ -13,6 +14,11 @@ class ProductTemplate(models.Model):
('service_to_purchase', "CHECK((type != 'service' AND service_to_purchase != true) or (type = 'service'))", 'Product that is not a service can not create RFQ.'),
]
@api.constrains('service_to_purchase', 'seller_ids')
def validate_service_to_purchase(self):
if self.service_to_purchase and not self.seller_ids:
raise ValidationError("Please define the vendor from whom you would like to purchase this service automatically.")
@api.onchange('type')
def _onchange_type(self):
res = super(ProductTemplate, self)._onchange_type()
+18 -13
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo import Command
class TestCommonSalePurchaseNoChart(TestSaleCommon):
@@ -24,6 +24,19 @@ class TestCommonSalePurchaseNoChart(TestSaleCommon):
'email': 'supplier.serv@supercompany.com',
})
cls.supplierinfo1 = cls.env['product.supplierinfo'].create({
'partner_id': cls.partner_vendor_service.id,
'price': 100,
'delay': 1,
})
cls.supplierinfo2 = cls.env['product.supplierinfo'].create({
'partner_id': cls.partner_vendor_service.id,
'price': 10,
'delay': 5,
})
# Create product
# When service_to_purser is True add the supplier i.e 'saller_ids' on the product to void the Validation error at product creation time
cls.service_purchase_1 = cls.env['product.product'].create({
'name': "Out-sourced Service 1",
'standard_price': 200.0,
@@ -38,6 +51,7 @@ class TestCommonSalePurchaseNoChart(TestSaleCommon):
'taxes_id': False,
'categ_id': cls.product_category_purchase.id,
'service_to_purchase': True,
'seller_ids': [Command.set(cls.supplierinfo1.ids)],
})
cls.service_purchase_2 = cls.env['product.product'].create({
'name': "Out-sourced Service 2",
@@ -53,17 +67,8 @@ class TestCommonSalePurchaseNoChart(TestSaleCommon):
'taxes_id': False,
'categ_id': cls.product_category_purchase.id,
'service_to_purchase': True,
'seller_ids': [Command.set(cls.supplierinfo2.ids)],
})
cls.supplierinfo1 = cls.env['product.supplierinfo'].create({
'partner_id': cls.partner_vendor_service.id,
'price': 100,
'product_tmpl_id': cls.service_purchase_1.product_tmpl_id.id,
'delay': 1,
})
cls.supplierinfo2 = cls.env['product.supplierinfo'].create({
'partner_id': cls.partner_vendor_service.id,
'price': 10,
'product_tmpl_id': cls.service_purchase_2.product_tmpl_id.id,
'delay': 5,
})
cls.supplierinfo1.product_tmpl_id = cls.service_purchase_1.product_tmpl_id.id
cls.supplierinfo2.product_tmpl_id = cls.service_purchase_2.product_tmpl_id.id
@@ -92,9 +92,9 @@ class TestSaleService(TestCommonSaleTimesheet):
# delete timesheets before deleting the task, so as to trigger the error
# about linked sales order lines and not the one about linked timesheets
task.timesheet_ids.unlink()
# not possible to delete a task linked to a SOL
with self.assertRaises(ValidationError):
task.unlink()
# unlink automatically task from the SOL when deleting the task
task.unlink()
self.assertFalse(sale_order_line.task_id, "Deleting the task its should automatically unlink the task from SOL.")
def test_timesheet_uom(self):
""" Test timesheet invoicing and uom conversion """
@@ -134,6 +134,17 @@
</field>
</record>
<record id="view_task_tree2_inherited" model="ir.ui.view">
<field name="name">project.task.tree.inherited</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="hr_timesheet.view_task_tree2_inherited" />
<field name="arch" type="xml">
<xpath expr="//field[@name='remaining_hours']" position="after">
<field name="remaining_hours_so" string="Remaining Hours on SO" widget="timesheet_uom" optional="hide"/>
</xpath>
</field>
</record>
<record id="project_task_view_form_inherit_sale_timesheet" model="ir.ui.view">
<field name="name">project.task.form.inherit.timesheet</field>
<field name="model">project.task</field>