[FW][MERGE] project: Project Sharing feature follow-up

This PR are the follow-up of #73341 PR to fix the various points to finalize the feature for the v.15.

These points are done only in the Project Sharing feature:
- remove import button in favorite menu
- remove some filters in search view and update the search view
- add uom in session to use timesheet_uom widget for timesheets: this widget uses the uom stored in the session to display the unit_amount in hours or in day format.
- remove the using of avatar widget for employees.
- remove checkboxes in list view.
- change label for group by user_ids
- not set by default the height of the chatter
- remove the 'Set a cover' button in kanban view because the portal user has no access to `ir.attachment` model.
- add unit tests for project sharing feature.
- force staying in the project sharing form view when the user wants to see a subtask.
- display subtask planned hours
- allow the portal user to edit priority task when he is in project sharing feature.
- show "Assign to me" button
- see custom kanban state label
- get all options when the chatter is reloaded in project sharing.

In classic portal views:
- use task in sudo for classic portal form view

General point:
- prevent click on avatar user when `noOpenChat` is `true`

task-2633229

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

closes odoo/odoo#79429

Forward-port-of: odoo/odoo#77156
Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
Odoo's Mergebot
2021-11-05 18:33:40 +01:00
committed by GitHub
19 changed files with 281 additions and 35 deletions
@@ -10,6 +10,31 @@ from odoo.addons.project.controllers.portal import CustomerPortal
class ProjectCustomerPortal(CustomerPortal):
def _prepare_project_sharing_session_info(self, project):
session_info = super()._prepare_project_sharing_session_info(project)
company = project.company_id
timesheet_encode_uom = company.timesheet_encode_uom_id
project_time_mode_uom = company.project_time_mode_id
session_info['user_companies']['allowed_companies'][company.id].update(
timesheet_uom_id=timesheet_encode_uom.id,
timesheet_uom_factor=project_time_mode_uom._compute_quantity(
1.0,
timesheet_encode_uom,
round=False
),
)
session_info['uom_ids'] = {
uom.id:
{
'id': uom.id,
'name': uom.name,
'rounding': uom.rounding,
'timesheet_widget': uom.timesheet_widget,
} for uom in [timesheet_encode_uom, project_time_mode_uom]
}
return session_info
def _task_get_page_view_values(self, task, access_token, **kwargs):
values = super(ProjectCustomerPortal, self)._task_get_page_view_values(task, access_token, **kwargs)
domain = request.env['account.analytic.line']._timesheet_get_portal_domain()
+3 -2
View File
@@ -163,9 +163,10 @@ class AccountAnalyticLine(models.Model):
def _apply_time_label(self, view_arch, related_model):
doc = etree.XML(view_arch)
Model = self.env[related_model]
encoding_uom = self.env.company.timesheet_encode_uom_id
# Just fetch the name of the uom in `timesheet_encode_uom_id` of the current company
encoding_uom_name = self.env.company.timesheet_encode_uom_id.with_context(prefetch_fields=False).sudo().name
for node in doc.xpath("//field[@widget='timesheet_uom'][not(@string)] | //field[@widget='timesheet_uom_no_toggle'][not(@string)]"):
name_with_uom = re.sub(_('Hours') + "|Hours", encoding_uom.name or '', Model._fields[node.get('name')]._description_string(self.env), flags=re.IGNORECASE)
name_with_uom = re.sub(_('Hours') + "|Hours", encoding_uom_name or '', Model._fields[node.get('name')]._description_string(self.env), flags=re.IGNORECASE)
node.set('string', name_with_uom)
return etree.tostring(doc, encoding='unicode')
@@ -25,11 +25,11 @@
<label for="planned_hours" string="Initially Planned Days" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
</div>
<field name="planned_hours" widget="timesheet_uom_no_toggle" nolabel="1"/>
<div class="o_td_label" groups="project.group_subtask_project" attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}">
<div class="o_td_label" attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}">
<label for="subtask_planned_hours" string="Sub-tasks Planned Hours" attrs="{'invisible': [('encode_uom_in_days', '=', True)]}"/>
<label for="subtask_planned_hours" string="Sub-tasks Planned Days" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
</div>
<field name="subtask_planned_hours" widget="timesheet_uom_no_toggle" nolabel="1" groups="project.group_subtask_project" attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}"/>
<field name="subtask_planned_hours" widget="timesheet_uom_no_toggle" nolabel="1" attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}"/>
</group>
<group>
<field name="progress" widget="progressbar"/>
@@ -37,18 +37,19 @@
</group>
<field name="timesheet_ids" mode="tree,kanban"
attrs="{'invisible': [('analytic_account_active', '=', False)]}">
<!-- TODO: see how to disable the _onRowclicked to not try to open form view -->
<tree string="Timesheet Activities" default_order="date" no_open="1">
<field name="date"/>
<!-- [XBO] TODO: remove me in master -->
<field name="user_id" invisible="1"/>
<field name="employee_id" widget="many2one_avatar_employee" options="{'no_open_chat': True}"/>
<field name="employee_id"/>
<field name="name"/>
<field name="unit_amount" widget="timesheet_uom" decoration-danger="unit_amount &gt; 24"/>
</tree>
<kanban class="o_kanban_mobile">
<field name="date"/>
<!-- [XBO] TODO: remove me in master -->
<field name="user_id" invisible="1"/>
<field name="employee_id" widget="many2one_avatar_employee" options="{'no_open_chat': True}"/>
<field name="employee_id"/>
<field name="name"/>
<field name="unit_amount" decoration-danger="unit_amount &gt; 24"/>
<field name="project_id"/>
@@ -154,8 +155,8 @@
<field name="effective_hours" widget="timesheet_uom" sum="Effective Hours" optional="show" attrs="{'column_invisible': [('allow_timesheets', '=', False)]}"/>
<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" attrs="{'column_invisible': [('allow_timesheets', '=', False)]}"/>
<field name="subtask_effective_hours" widget="timesheet_uom" attrs="{'column_invisible' : ['|', ('allow_subtasks', '=', False), ('allow_timesheets', '=', False)]}" optional="hide"/>
<field name="total_hours_spent" widget="timesheet_uom" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('allow_timesheets', '=', False)]}" optional="hide"/>
<field name="progress" widget="progressbar" optional="show" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="total_hours_spent" widget="timesheet_uom" attrs="{'column_invisible' : ['|', ('allow_subtasks', '=', False), ('allow_timesheets', '=', False)]}" optional="hide"/>
<field name="progress" widget="progressbar" attrs="{'column_invisible': [('allow_timesheets', '=', False)]}" optional="show"/>
</field>
</field>
</record>
@@ -122,7 +122,7 @@ var PortalChatter = publicWidget.Widget.extend({
*/
_setOptions: function (options) {
// underscorize the camelcased option keys
const defaultOptions = {
const defaultOptions = Object.assign({
'allow_composer': true,
'display_composer': false,
'csrf_token': odoo.csrf_token,
@@ -137,7 +137,7 @@ var PortalChatter = publicWidget.Widget.extend({
'pid': false,
'domain': [],
'two_columns': false,
};
}, this.options || {});
this.options = Object.entries(options).reduce(
(acc, [key, value]) => {
+4 -1
View File
@@ -87,7 +87,9 @@
'project/static/src/js/portal_rating.js',
],
'web.assets_qweb': [
'project/static/src/**/*.xml',
'project/static/src/xml/**/*',
'project/static/src/burndown_chart/**/*.xml',
'project/static/src/project_control_panel/**/*.xml',
],
'web.qunit_suite_tests': [
'project/static/tests/burndown_chart_tests.js',
@@ -115,6 +117,7 @@
'web/static/src/legacy/js/public/public_widget.js',
'portal/static/src/js/portal_chatter.js',
'portal/static/src/js/portal_composer.js',
'project/static/src/project_sharing/search/favorite_menu/custom_favorite_item.xml',
'project/static/src/project_sharing/**/*.js',
'project/static/src/scss/project_sharing/*',
'web/static/src/start.js',
+14 -10
View File
@@ -159,12 +159,7 @@ class ProjectCustomerPortal(CustomerPortal):
values['task_url'] = 'project/%s/task' % project_id
return request.render("project.portal_my_project", values)
@http.route("/my/project/<int:project_id>/project_sharing", type="http", auth="user", methods=['GET'])
def render_project_backend_view(self, project_id):
project = request.env['project.project'].sudo().browse(project_id)
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
return request.not_found()
def _prepare_project_sharing_session_info(self, project):
session_info = request.env['ir.http'].session_info()
user_context = request.session.get_context() if request.session.uid else {}
mods = conf.server_wide_modules or []
@@ -190,11 +185,20 @@ class ProjectCustomerPortal(CustomerPortal):
},
},
},
# FIXME: See if we prefer to give only the currency that the portal user just need to see the correct information in project sharing
currencies=request.env['ir.http'].get_currencies(),
)
return session_info
@http.route("/my/project/<int:project_id>/project_sharing", type="http", auth="user", methods=['GET'])
def render_project_backend_view(self, project_id):
project = request.env['project.project'].sudo().browse(project_id)
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
return request.not_found()
return request.render(
'project.project_sharing_embed',
{'session_info': session_info},
{'session_info': self._prepare_project_sharing_session_info(project)},
)
@http.route('/my/project/<int:project_id>/task/<int:task_id>', type='http', auth='public', website=True)
@@ -206,9 +210,9 @@ class ProjectCustomerPortal(CustomerPortal):
Task = request.env['project.task']
if access_token:
Task = Task.sudo()
task = Task.search([('project_id', '=', project_id), ('id', '=', task_id)], limit=1)
task.sudo().attachment_ids.generate_access_token()
values = self._task_get_page_view_values(task, access_token, project=project_sudo, **kw)
task_sudo = Task.search([('project_id', '=', project_id), ('id', '=', task_id)], limit=1).sudo()
task_sudo.attachment_ids.generate_access_token()
values = self._task_get_page_view_values(task_sudo, access_token, project=project_sudo, **kw)
values['project'] = project_sudo
return request.render("project.portal_my_task", values)
+4 -1
View File
@@ -38,8 +38,10 @@ PROJECT_TASK_READABLE_FIELDS = {
'company_id',
'displayed_image_id',
'display_name',
'priority',
'portal_user_names',
'legend_normal',
'legend_blocked',
'legend_done',
}
PROJECT_TASK_WRITABLE_FIELDS = {
@@ -54,6 +56,7 @@ PROJECT_TASK_WRITABLE_FIELDS = {
'kanban_state',
'child_ids',
'parent_id',
'priority',
}
class ProjectTaskType(models.Model):
@@ -0,0 +1,35 @@
/** @odoo-module **/
import FavoriteMenuLegacy from 'web.FavoriteMenu';
import CustomFavoriteItemLegacy from 'web.CustomFavoriteItem';
import { registry } from "@web/core/registry";
/**
* Remove all components contained in the favorite menu registry except the CustomFavoriteItem
* component for only the project sharing feature.
*/
export function prepareFavoriteMenuRegister() {
let customFavoriteItemKey = 'favorite-generator-menu';
const keys = FavoriteMenuLegacy.registry.keys().filter(key => key !== customFavoriteItemKey);
FavoriteMenuLegacy.registry = Object.assign(FavoriteMenuLegacy.registry, {
map: {},
_scoreMapping: {},
_sortedKeys: null,
});
FavoriteMenuLegacy.registry.add(customFavoriteItemKey, CustomFavoriteItemLegacy, 0);
// notify the listeners, we keep only one key in this registry.
for (const key of keys) {
for (const callback of FavoriteMenuLegacy.registry.listeners) {
callback(key, undefined);
}
}
customFavoriteItemKey = 'custom-favorite-item';
const favoriteMenuRegistry = registry.category("favoriteMenu");
for (const [key, _] of favoriteMenuRegistry.getEntries()) {
if (key !== customFavoriteItemKey) {
favoriteMenuRegistry.remove(key);
}
}
}
@@ -1,5 +1,7 @@
/** @odoo-module **/
import { startWebClient } from '@web/start';
import { ProjectSharingWebClient } from './project_sharing';
import { prepareFavoriteMenuRegister } from './components/favorite_menu_registry';
prepareFavoriteMenuRegister();
startWebClient(ProjectSharingWebClient);
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-inherit="web.CustomFavoriteItem" t-inherit-mode="extension">
<xpath expr="//CheckBox[@value='state.isShared']" position="replace"/>
</t>
</templates>
@@ -7,4 +7,40 @@ export default ListView.extend({
config: Object.assign({}, ListView.prototype.config, {
Controller,
}),
init: function (viewInfo, params) {
return this._super(viewInfo, {...params, hasSelectors: false});
},
getRenderer (parent, state) {
const columnInvisibleFields = {};
for (const child of this.arch.children) {
if (child.attrs && child.attrs.modifiers && child.attrs.modifiers.column_invisible) {
columnInvisibleFields[child.attrs.name] = child.attrs.modifiers.column_invisible;
}
}
this.rendererParams.columnInvisibleFields = Object.entries(columnInvisibleFields).reduce((acc, entry) => {
const [fieldName, domains] = entry;
if (domains instanceof Array && state.data.length) {
const record = state.data[0];
const data = Object.entries(record.data).reduce((acc, entry) => {
const [fieldName, value] = entry;
const field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
if (value instanceof Object && value.type === 'list') {
acc[fieldName] = value.id;
}
}
return acc;
}, record.data);
if (Object.keys(data).length) {
// We assume the field in this domain is a related to the project shared.
acc[fieldName] = this.model._evalModifiers({ ...record, data }, { column_invisible: domains }).column_invisible;
}
} else {
acc[fieldName] = domains;
}
return acc;
}, {});
return this._super(parent, state);
},
});
@@ -2,7 +2,6 @@
display: flex;
background-color: $white;
border-color: $border-color;
height: fit-content;
.o_portal_chatter {
width: 100%;
@@ -69,6 +69,24 @@ tour.register('project_sharing_tour', {
}, {
trigger: 'ol.breadcrumb > li.o_back_button > a:contains(Project Sharing)',
content: 'Go back to the kanban view',
}, {
trigger: '.o_filter_menu > button',
content: 'click on filter menu in the search view',
}, {
trigger: '.o_filter_menu > .dropdown-menu > .dropdown-item:first-child',
content: 'click on the first item in the filter menu',
}, {
trigger: '.o_group_by_menu > button',
content: 'click on group by menu in the search view',
}, {
trigger: '.o_group_by_menu > .dropdown-menu > .dropdown-item:first-child',
content: 'click on the first item in the group by menu',
}, {
trigger: '.o_favorite_menu > button',
content: 'click on the favorite menu in the search view',
}, {
trigger: '.o_favorite_menu .o_add_favorite > button',
content: 'click to "save current search" button in favorite menu',
}, {
trigger: 'button.o_switch_view.o_list',
content: 'Go to the list view',
@@ -109,6 +127,24 @@ tour.register('portal_project_sharing_tour', {
}, {
trigger: 'ol.breadcrumb > li.o_back_button > a:contains(Project Sharing)',
content: 'Go back to the kanban view',
}, {
trigger: '.o_filter_menu > button',
content: 'click on filter menu in the search view',
}, {
trigger: '.o_filter_menu > .dropdown-menu > .dropdown-item:first-child',
content: 'click on the first item in the filter menu',
}, {
trigger: '.o_group_by_menu > button',
content: 'click on group by menu in the search view',
}, {
trigger: '.o_group_by_menu > .dropdown-menu > .dropdown-item:first-child',
content: 'click on the first item in the group by menu',
}, {
trigger: '.o_favorite_menu > button',
content: 'click on the favorite menu in the search view',
}, {
trigger: '.o_favorite_menu .o_add_favorite > button',
content: 'click to "save current search" button in favorite menu',
}, {
trigger: 'button.o_switch_view.o_list',
content: 'Go to the list view',
+1
View File
@@ -7,6 +7,7 @@ from . import test_project_config
from . import test_project_flow
from . import test_project_recurrence
from . import test_project_sharing
from . import test_project_sharing_portal_access
from . import test_project_sharing_ui
from . import test_project_subtasks
from . import test_project_ui
+3 -1
View File
@@ -51,10 +51,12 @@ class TestProjectSharingCommon(TestProjectCommon):
'project_id': cls.project_portal.id,
})
cls.project_sharing_form_view_xml_id = 'project.project_sharing_project_task_view_form'
def get_project_sharing_form_view(self, record, with_user=None):
return Form(
record.with_user(with_user or self.env.user),
view="project.project_sharing_project_task_view_form"
view=self.project_sharing_form_view_xml_id
)
@tagged('project_sharing')
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import OrderedDict
from odoo import Command
from odoo.exceptions import AccessError
from odoo.tests import tagged
from .test_project_sharing import TestProjectSharingCommon
@tagged('post_install', '-at_install')
class TestProjectSharingPortalAccess(TestProjectSharingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
project_share_wizard = cls.env['project.share.wizard'].create({
'access_mode': 'edit',
'res_model': 'project.project',
'res_id': cls.project_portal.id,
'partner_ids': [
Command.link(cls.partner_portal.id),
],
})
project_share_wizard.action_send_mail()
Task = cls.env['project.task']
cls.read_protected_fields_task = OrderedDict([
(k, v)
for k, v in Task._fields.items()
if k in Task.SELF_READABLE_FIELDS
])
cls.write_protected_fields_task = OrderedDict([
(k, v)
for k, v in Task._fields.items()
if k in Task.SELF_WRITABLE_FIELDS
])
cls.readonly_protected_fields_task = OrderedDict([
(k, v)
for k, v in Task._fields.items()
if k in Task.SELF_READABLE_FIELDS and k not in Task.SELF_WRITABLE_FIELDS
])
cls.other_fields_task = OrderedDict([
(k, v)
for k, v in Task._fields.items()
if k not in Task.SELF_READABLE_FIELDS
])
def test_readonly_fields(self):
""" The fields are not writeable should not be editable by the portal user. """
view_infos = self.task_portal.fields_view_get(view_id=self.env.ref(self.project_sharing_form_view_xml_id).id)
project_task_fields = {
field_name
for field_name, field_attrs in view_infos['fields'].items()
if field_name not in self.write_protected_fields_task
}
with self.get_project_sharing_form_view(self.task_portal, self.user_portal) as form:
for field in project_task_fields:
with self.assertRaises(AssertionError, msg="Field '%s' should be readonly in the project sharing form view "):
form.__setattr__(field, 'coucou')
def test_read_task_with_portal_user(self):
self.task_portal.with_user(self.user_portal).read(self.read_protected_fields_task)
with self.assertRaises(AccessError):
self.task_portal.with_user(self.user_portal).read(self.other_fields_task)
def test_write_with_portal_user(self):
for field in self.readonly_protected_fields_task:
with self.assertRaises(AccessError):
self.task_portal.with_user(self.user_portal).write({field: 'dummy'})
for field in self.other_fields_task:
with self.assertRaises(AccessError):
self.task_portal.with_user(self.user_portal).write({field: 'dummy'})
+19 -5
View File
@@ -27,6 +27,7 @@
on_create="quick_create"
quick_create_view="project.project_sharing_quick_create_task_form"
archivable="0"
import="0"
>
<field name="color"/>
<field name="priority"/>
@@ -42,6 +43,9 @@
<field name="active"/>
<field name="allow_subtasks"/>
<field name="child_text"/>
<field name="legend_blocked" invisible="1"/>
<field name="legend_normal" invisible="1"/>
<field name="legend_done" invisible="1"/>
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger", "normal": "muted"}'/>
<templates>
<t t-name="kanban-box">
@@ -70,7 +74,8 @@
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
<a t-if="widget.editable" role="menuitem" type="set_cover" class="dropdown-item" data-field="displayed_image_id">Set Cover Image</a>
<!-- [XBO] TODO: remove me in master -->
<a t-if="widget.editable" role="menuitem" type="set_cover" class="dropdown-item d-none" data-field="displayed_image_id">Set Cover Image</a>
<a t-if="widget.editable" role="menuitem" type="edit" class="dropdown-item">Edit</a>
<div role="separator" class="dropdown-divider"></div>
<ul class="oe_kanban_colorpicker" data-field="color"/>
@@ -115,7 +120,7 @@
<field name="model">project.task</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<tree string="Tasks" sample="1" delete="0">
<tree string="Tasks" sample="1" delete="0" import="0">
<field name="is_closed" invisible="1" />
<field name="allow_subtasks" invisible="1" />
<field name="sequence" invisible="1" readonly="1"/>
@@ -130,6 +135,9 @@
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
<field name="kanban_state" widget="state_selection" optional="hide"/>
<field name="legend_blocked" invisible="1"/>
<field name="legend_normal" invisible="1"/>
<field name="legend_done" invisible="1"/>
<field name="stage_id" invisible="context.get('set_visible',False)" optional="show"/>
</tree>
</field>
@@ -159,6 +167,9 @@
<field name="priority" widget="priority" class="mr-3"/>
<field name="name" class="o_task_name text-truncate" placeholder="Task Title..."/>
<field name="kanban_state" widget="state_selection" class="ml-auto"/>
<field name="legend_blocked" invisible="1"/>
<field name="legend_normal" invisible="1"/>
<field name="legend_done" invisible="1"/>
</h1>
</div>
<group>
@@ -186,7 +197,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': [(6, 0, [uid])], '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': [(6, 0, [uid])], 'default_parent_id': id, 'default_partner_id': partner_id, 'form_view_ref' : 'project.project_sharing_project_task_view_form'}">
<tree editable="bottom">
<field name="project_id" invisible="1"/>
<field name="is_closed" invisible="1"/>
@@ -232,6 +243,7 @@
<filter string="Unassigned" name="unassigned" domain="[('user_ids', '=', False)]"/>
<separator/>
<filter string="Starred" name="starred" domain="[('priority', 'in', [1, 2])]"/>
<separator/>
<filter string="Late Tasks" name="late" domain="[('date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"/>
<separator/>
<filter invisible="1" string="Today Activities" name="activities_today"
@@ -242,11 +254,13 @@
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
<!-- TODO: [XBO] remove me in master -->
<filter string="Assignees" name="user" context="{'group_by': 'user_ids'}" invisible="1"/>
<filter string="Project" name="project" context="{'group_by': 'project_id'}"/>
<!-- [XBO] TODO: remove me in master -->
<filter string="Project" name="project" context="{'group_by': 'project_id'}" invisible="1"/>
<filter string="Customer" name="customer" context="{'group_by': 'partner_id'}"/>
<filter string="Kanban State" name="kanban_state" context="{'group_by': 'kanban_state'}"/>
<filter string="Deadline" name="date_deadline" context="{'group_by': 'date_deadline'}"/>
<filter string="Creation Date" name="group_create_date" context="{'group_by': 'create_date'}"/>
<!-- [XBO] TODO: remove me in master -->
<filter string="Creation Date" name="group_create_date" context="{'group_by': 'create_date'}" invisible="1"/>
</group>
</search>
</field>
+2 -2
View File
@@ -809,7 +809,7 @@
<field name="rating_count" invisible="1"/>
<header>
<button name="action_assign_to_me" string="Assign to Me" type="object" class="oe_highlight"
attrs="{'invisible' : [('user_ids', '!=', [])]}" data-hotkey="q"/>
attrs="{'invisible' : &quot;[('user_ids', '=', uid)]&quot;}" data-hotkey="q"/>
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}"/>
</header>
<div class="alert alert-info oe_edit_only mb-0" role="status" attrs="{'invisible': ['|', ('recurring_task', '=', False), ('recurrence_id', '=', False)]}">
@@ -1100,7 +1100,7 @@
<t t-else="">
<br/>
<span class="font-italic text-muted"><i class="fa fa-lock"></i> Private</span>
</t>
</t>
<br />
<t t-if="record.partner_id.value">
<span t-if="!record.partner_is_company.raw_value">
@@ -8,9 +8,9 @@
<field name="arch" type="xml">
<div name="button_box" position="inside">
<field name="sale_order_id" invisible="1"/>
<button class="d-none d-md-inline oe_stat_button o_debounce_disabled"
type="object" name="action_view_so" icon="fa-dollar" special="cancel"
attrs="{'invisible': [('sale_order_id', '=', False)]}"
<button class="d-none d-md-inline oe_stat_button"
type="object" name="action_view_so" icon="fa-dollar"
invisible="1"
string="Sales Order"/>
</div>
<xpath expr="//field[@name='partner_id']" position="attributes">