[IMP] project: select access rights in project sharing
Before this commit, when the portal user can access to project sharing views, he can just read. If we just add ir.rule to allow the edition, the portal could create/edit. We have to allow the project manager to define the access rights of portal users when he want to share them a project with the project sharing feature. This commit adds 3 access modes portal users in project sharing views. 1. **Readonly**: the portal can only see the tasks in the different views. 2. **Comment**: the portal can use the chatter in task form view. 3. **Edit**: the portal user can create and edit tasks. task-2379518 closes #73341 Co-Authored-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
co-authored by
Yannick Tivisse
parent
716f47d14b
commit
5ea5ddafea
@@ -32,6 +32,7 @@
|
||||
'views/project_update_views.xml',
|
||||
'views/project_update_templates.xml',
|
||||
'views/project_project_stage_views.xml',
|
||||
'wizard/project_share_wizard_views.xml',
|
||||
'views/project_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
|
||||
@@ -104,8 +104,7 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||
@http.route("/embed/project/<int:project_id>", 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():
|
||||
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
|
||||
return request.not_found()
|
||||
|
||||
session_info = request.env['ir.http'].session_info()
|
||||
@@ -124,6 +123,7 @@ class ProjectCustomerPortal(CustomerPortal):
|
||||
cache_hashes=cache_hashes,
|
||||
action_name='project.project_sharing_project_task_action',
|
||||
project_id=project.id,
|
||||
can_edit=project.user_can_edit,
|
||||
user_companies={
|
||||
'current_company': project_company.id,
|
||||
'allowed_companies': {
|
||||
|
||||
@@ -6,6 +6,7 @@ from . import project_milestone
|
||||
from . import project_project_stage
|
||||
from . import project_task_recurrence
|
||||
from . import project
|
||||
from . import project_sharing_access
|
||||
from . import project_update
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
|
||||
@@ -9,10 +9,11 @@ from random import randint
|
||||
|
||||
from odoo import api, fields, models, tools, SUPERUSER_ID, _
|
||||
from odoo.exceptions import UserError, ValidationError, AccessError
|
||||
from odoo.osv.expression import OR
|
||||
from odoo.osv import expression
|
||||
|
||||
from .project_task_recurrence import DAYS, WEEKS
|
||||
from .project_update import STATUS_COLOR
|
||||
from .project_sharing_access import PROJECT_SHARING_ACCESS_MODE
|
||||
|
||||
|
||||
PROJECT_TASK_READABLE_FIELDS = {
|
||||
@@ -37,6 +38,7 @@ PROJECT_TASK_READABLE_FIELDS = {
|
||||
'displayed_image_id',
|
||||
'display_name',
|
||||
'priority',
|
||||
'project_sharing_access_mode',
|
||||
}
|
||||
|
||||
PROJECT_TASK_WRITABLE_FIELDS = {
|
||||
@@ -280,6 +282,9 @@ class Project(models.Model):
|
||||
allow_task_dependencies = fields.Boolean('Task Dependencies', default=lambda self: self.env.user.has_group('project.group_project_task_dependencies'))
|
||||
tag_ids = fields.Many2many('project.tags', relation='project_project_project_tags_rel', string='Tags')
|
||||
|
||||
# Project Sharing fields
|
||||
project_sharing_access_ids = fields.One2many('project.sharing.access', 'project_id', 'Project Sharing Access', copy=False)
|
||||
|
||||
# rating fields
|
||||
rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True)
|
||||
rating_active = fields.Boolean('Customer Ratings', default=lambda self: self.env.user.has_group('project.group_project_rating'))
|
||||
@@ -695,6 +700,25 @@ class Project(models.Model):
|
||||
})
|
||||
project.write({'analytic_account_id': analytic_account.id})
|
||||
|
||||
def _check_project_sharing_access(self, access_mode='read'):
|
||||
self.ensure_one()
|
||||
if access_mode not in map(lambda access: access[0], PROJECT_SHARING_ACCESS_MODE):
|
||||
raise ValidationError('The %s does not exists in project sharing accesses.' % access_mode)
|
||||
if self.privacy_visibility != 'portal' or self.env.user._is_public():
|
||||
return False
|
||||
if self.env.user.has_group('base.group_portal'):
|
||||
# check in project sharing access model of the project
|
||||
project_sharing_access = self.project_sharing_access_ids.filtered(lambda access: access.user_id == self.env.user)
|
||||
if not project_sharing_access:
|
||||
return False
|
||||
if access_mode == 'edit':
|
||||
return project_sharing_access.access_mode == 'edit'
|
||||
elif access_mode == 'comment':
|
||||
return project_sharing_access.access_mode in ('comment', 'edit')
|
||||
else:
|
||||
return True
|
||||
return self.env.user.has_group('base.group_user')
|
||||
|
||||
# ---------------------------------------------------
|
||||
# Rating business
|
||||
# ---------------------------------------------------
|
||||
@@ -856,6 +880,9 @@ class Task(models.Model):
|
||||
allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies')
|
||||
depend_on_ids = fields.Many2many('project.task', relation="task_dependencies_rel", column1="task_id", column2="depends_on_id", string="Blocked By", domain="[('allow_task_dependencies', '=', True), ('id', '!=', id)]")
|
||||
|
||||
# Project Sharing fields
|
||||
project_sharing_access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, compute='_compute_project_sharing_access_mode')
|
||||
|
||||
# recurrence fields
|
||||
allow_recurring_tasks = fields.Boolean(related='project_id.allow_recurring_tasks')
|
||||
recurring_task = fields.Boolean(string="Recurrent")
|
||||
@@ -1162,6 +1189,26 @@ class Task(models.Model):
|
||||
else:
|
||||
task.stage_id = False
|
||||
|
||||
@api.depends_context('uid')
|
||||
@api.depends('project_id.project_sharing_access_ids', 'project_id.privacy_visibility')
|
||||
def _compute_project_sharing_access_mode(self):
|
||||
if self.env.user._is_public():
|
||||
self.project_sharing_access_mode = False
|
||||
return
|
||||
portal_user = self.user_has_groups('base.group_portal')
|
||||
project_sharings = self.project_id.filtered(lambda project: project.privacy_visibility == 'portal')
|
||||
if portal_user:
|
||||
project_sharing_access_read = self.env['project.sharing.access'].search_read(
|
||||
[('user_id', '=', self.env.user.id), ('project_id', 'in', project_sharings.ids)],
|
||||
['project_id', 'access_mode']
|
||||
)
|
||||
project_sharing_access_dict = {res['project_id'][0]: res['access_mode'] for res in project_sharing_access_read}
|
||||
for task in self:
|
||||
if not portal_user:
|
||||
task.project_sharing_access_mode = task.project_id in project_sharings and 'edit'
|
||||
continue
|
||||
task.project_sharing_access_mode = project_sharing_access_dict.get(task.project_id.id, False)
|
||||
|
||||
@api.constrains('recurring_task')
|
||||
def _check_recurring_task(self):
|
||||
if self.filtered(lambda task: task.parent_id and task.recurring_task):
|
||||
@@ -1241,10 +1288,10 @@ class Task(models.Model):
|
||||
readable_fields = self.SELF_READABLE_FIELDS
|
||||
public_fields = {field_name: description for field_name, description in fields.items() if field_name in readable_fields}
|
||||
|
||||
writable_fields = self.SELF_WRITABLE_FIELDS
|
||||
for field_name, description in public_fields.items():
|
||||
if field_name not in writable_fields and not description.get('readonly', False):
|
||||
# If the field is not in Writable fields and it is not readonly then we force the readonly to True
|
||||
writable_fields = self.SELF_WRITABLE_FIELDS
|
||||
for field_name, description in public_fields.items():
|
||||
if field_name not in writable_fields and not description.get('readonly', False):
|
||||
# If the field is not in Writable fields and it is not readonly then we force the readonly to True
|
||||
description['readonly'] = True
|
||||
|
||||
return public_fields
|
||||
@@ -1412,7 +1459,7 @@ class Task(models.Model):
|
||||
recurrence_domain = []
|
||||
if recurrence_update == 'subsequent':
|
||||
for task in self:
|
||||
recurrence_domain = OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]])
|
||||
recurrence_domain = expression.OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]])
|
||||
else:
|
||||
recurrence_domain = [('recurrence_id', 'in', self.recurrence_id.ids)]
|
||||
tasks |= self.env['project.task'].search(recurrence_domain)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
PROJECT_SHARING_ACCESS_MODE = [
|
||||
('read', 'Read'),
|
||||
('comment', 'Comment'),
|
||||
('edit', 'Edit'),
|
||||
]
|
||||
|
||||
|
||||
class ProjectSharingAccess(models.Model):
|
||||
_name = 'project.sharing.access'
|
||||
_description = 'Project Sharing Access'
|
||||
|
||||
project_id = fields.Many2one('project.project', 'Project Shared', required=True)
|
||||
user_id = fields.Many2one('res.users', string='Portal Users', domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_portal').id)], required=True)
|
||||
access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, string='Access Mode', required=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('unique_user', 'UNIQUE(project_id, user_id)', 'A user cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'),
|
||||
]
|
||||
@@ -36,3 +36,7 @@ access_project_milestone_user,project.milestone.user,model_project_milestone,bas
|
||||
access_project_milestone_portal,project.milestone.portal,model_project_milestone,base.group_portal,1,0,0,0
|
||||
access_project_milestone_project_user,project.milestone.project.user,model_project_milestone,project.group_project_user,1,0,0,0
|
||||
access_project_milestone_project_manager,project.milestone.project.manager,model_project_milestone,project.group_project_manager,1,1,1,1
|
||||
access_project_share_project_manager,project.share.wizard.project.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0
|
||||
access_project_share_wizard_line_project_manager,project.share.wizard.line.project.manager,model_project_share_wizard_line,project.group_project_manager,1,1,1,0
|
||||
access_project_sharing_access_project_manager,project.sharing.access.project.manager,model_project_sharing_access,project.group_project_manager,1,1,1,1
|
||||
access_project_sharing_access_project_portal,project.sharing.access.project.portal,model_project_sharing_access,base.group_portal,1,0,0,0
|
||||
|
||||
|
@@ -123,14 +123,33 @@
|
||||
<field name="name">Project/Task: portal users: (portal and following project) or (portal and following task)</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
'|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project_task_rule_portal_project_sharing" model="ir.rule">
|
||||
<field name="name">Project/Task: portal users: portal user can edit with project sharing feature</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
'|',
|
||||
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
('project_id.project_sharing_access_ids.user_id', 'in', [user.id]),
|
||||
('project_id.project_sharing_access_ids.access_mode', '=', 'edit'),
|
||||
]</field>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
@@ -162,5 +181,12 @@
|
||||
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project_sharing_access_portal" model="ir.rule">
|
||||
<field name="name">Project Sharing Access: Portal user can read its own access</field>
|
||||
<field name="model_id" ref="project.model_project_sharing_access" />
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -56,13 +56,15 @@ export class ProjectSharingWebClient extends Component {
|
||||
}
|
||||
|
||||
async _showView() {
|
||||
const { action_name, project_id } = session;
|
||||
const { action_name, project_id, can_edit } = session;
|
||||
await this.actionService.doAction(
|
||||
action_name,
|
||||
{
|
||||
clearBreadcrumbs: true,
|
||||
additionalContext: {
|
||||
active_id: project_id,
|
||||
create: can_edit,
|
||||
edit: can_edit,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project Sharing: Task" class="o_form_project_tasks">
|
||||
<header>
|
||||
<field name="stage_id" widget="statusbar" options="{'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)]}" />
|
||||
<field name="project_sharing_access_mode" invisible="1"/>
|
||||
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" attrs="{'invisible': [('project_id', '=', False), ('stage_id', '=', False)], 'readonly': [('project_sharing_access_mode', '!=', 'edit')]}" />
|
||||
</header>
|
||||
<sheet string="Task">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
@@ -143,7 +144,7 @@
|
||||
<h1 class="d-flex flex-row justify-content-between">
|
||||
<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" readonly="1"/>
|
||||
<field name="kanban_state" widget="state_selection" class="ml-auto" attrs="{'readonly': [('project_sharing_access_mode', '!=', 'edit')]}"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
@@ -194,7 +195,7 @@
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter"/>
|
||||
<div class="oe_project_sharing_chatter" attrs="{'invisible': [('project_sharing_access_mode', 'not in', ['comment', 'edit'])]}"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Project" delete="0">
|
||||
<header>
|
||||
<button name="%(portal.portal_share_action)d" string="Share" type="action" class="oe_highlight" groups="project.group_project_manager"
|
||||
<button name="%(project.project_share_action)d" string="Share" type="action" class="oe_highlight" groups="project.group_project_manager"
|
||||
attrs="{'invisible': [('privacy_visibility', '!=', 'portal')]}"/>
|
||||
<field name="stage_id" widget="statusbar" options="{'clickable': '1', 'fold_field': 'fold'}" groups="project.group_project_stages"/>
|
||||
</header>
|
||||
@@ -640,7 +640,7 @@
|
||||
<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="%(portal.portal_share_action)d" type="action">Share</a>
|
||||
<a t-if="record.privacy_visibility.raw_value == 'portal'" class="dropdown-item" role="menuitem" name="%(project.project_share_action)d" type="action">Share</a>
|
||||
<a class="dropdown-item" role="menuitem" type="edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import project_delete_wizard
|
||||
from . import project_task_type_delete
|
||||
from . import project_share_wizard
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, Command, fields, models, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
from ..models.project_sharing_access import PROJECT_SHARING_ACCESS_MODE
|
||||
|
||||
|
||||
class ProjectShareWizard(models.TransientModel):
|
||||
_name = 'project.share.wizard'
|
||||
_inherit = 'portal.share'
|
||||
_description = 'Project Share'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
result = super().default_get(fields_list)
|
||||
if result['res_model'] and result['res_id']:
|
||||
project = self.env[result['res_model']].browse(result['res_id'])
|
||||
result.update(
|
||||
line_ids=[
|
||||
Command.create(
|
||||
{'user_id': access.user_id, 'access_mode': access.access_mode}
|
||||
) for access in project.project_sharing_access_ids
|
||||
],
|
||||
)
|
||||
return result
|
||||
|
||||
partner_ids = fields.Many2many(compute="_compute_partner_ids", readonly=False, store=True, required=False)
|
||||
line_ids = fields.One2many('project.share.wizard.line', 'project_share_id')
|
||||
|
||||
@api.depends('line_ids.user_id')
|
||||
def _compute_partner_ids(self):
|
||||
for project_share_wizard in self:
|
||||
project_share_wizard.partner_ids |= project_share_wizard.line_ids.user_id.partner_id
|
||||
|
||||
def action_send_mail_and_confirm_access(self):
|
||||
if not self.partner_ids:
|
||||
raise UserError(_('Impossible to send email, no recipients are mentionned.'))
|
||||
self.action_confirm_access()
|
||||
return self.action_send_mail()
|
||||
|
||||
def action_confirm_access(self):
|
||||
project = self.env[self.res_model].browse(self.res_id)
|
||||
if not project or not project.exists():
|
||||
raise ValidationError(_('Impossible to confirm the project sharing access, the project is not found or project sharing is disabled for this project.'))
|
||||
project_sharing_access_dict = {access.user_id.id: access for access in project.project_sharing_access_ids}
|
||||
project_sharing_access_vals_list = [
|
||||
Command.delete(access.id)
|
||||
for user_id, access in project_sharing_access_dict.items()
|
||||
if user_id not in self.line_ids.user_id.ids
|
||||
]
|
||||
for share_access in self.line_ids:
|
||||
project_sharing_access = project_sharing_access_dict.get(share_access.user_id.id)
|
||||
if not project_sharing_access:
|
||||
project_sharing_access_vals_list.append(
|
||||
Command.create(
|
||||
{'user_id': share_access.user_id.id, 'access_mode': share_access.access_mode}
|
||||
)
|
||||
)
|
||||
elif project_sharing_access.access_mode != share_access.access_mode:
|
||||
project_sharing_access_vals_list.append(Command.update(project_sharing_access.id, {'access_mode': share_access.access_mode}))
|
||||
project.write({'project_sharing_access_ids': project_sharing_access_vals_list})
|
||||
project.message_subscribe(self.line_ids.user_id.partner_id.ids)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
class ProjectShareAccessWizard(models.TransientModel):
|
||||
_name = 'project.share.wizard.line'
|
||||
_description = 'Project Share Access'
|
||||
|
||||
project_share_id = fields.Many2one('project.share.wizard', required=True, ondelete='cascade')
|
||||
user_id = fields.Many2one('res.users', string='Portal Users', domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_portal').id)], required=True)
|
||||
access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, string='Access Mode', required=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('uniqueness_user', 'UNIQUE(project_share_id, user_id)', 'An user cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_share_wizard" model="ir.ui.view">
|
||||
<field name="name">project.share.wizard</field>
|
||||
<field name="model">project.share.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Share Document">
|
||||
<p class="alert alert-warning" attrs="{'invisible': [('access_warning', '=', '')]}" role="alert"><field name="access_warning"/></p>
|
||||
<group name="share_link">
|
||||
<field name="res_model" invisible="1"/>
|
||||
<field name="res_id" invisible="1"/>
|
||||
<field name="share_link" widget="CopyClipboardChar" options="{'string': 'Copy Link'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to share the document..."/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="note" placeholder="Add a note"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="line_ids" nolabel="1">
|
||||
<tree create="1" edit="1" delete="1" editable="bottom">
|
||||
<field name="user_id" />
|
||||
<field name="access_mode" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Confirm Access" name="action_confirm_access" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('line_ids', '=', [])]}" type="object" class="btn-primary" data-hotkey="v"/>
|
||||
<button string="Send" name="action_send_mail" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('line_ids', '!=', [])]}" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Send & Confirm" name="action_send_mail_and_confirm_access" attrs="{'invisible': ['|', ('access_warning', '!=', ''), ('line_ids', '=', [])]}" type="object" class="btn-primary" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" data-hotkey="z" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_share_action" model="ir.actions.act_window">
|
||||
<field name="name">Share Project</field>
|
||||
<field name="res_model">project.share.wizard</field>
|
||||
<field name="binding_model_id" ref="model_project_share_wizard"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user