[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:
@@ -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()
|
||||
|
||||
@@ -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 > 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 > 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 >= 100" decoration-warning="progress >= 80 and progress < 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]) => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+8
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'})
|
||||
@@ -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', '<', 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>
|
||||
|
||||
@@ -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' : "[('user_ids', '=', uid)]"}" 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">
|
||||
|
||||
Reference in New Issue
Block a user