[ADD] website_project: Portal access to Projects
Split project to move portal features to a new module named website_project. Portal users may now access their projects and tasks throught the website_portal My account page. The Backend portal menuitem is removed. Simplify the privacy settings of project, to be visible to portal users the project must be in 'portal' mode. Original authors: Vipul Bhatt, Florian Wintjens
This commit is contained in:
@@ -13,7 +13,6 @@
|
||||
'product',
|
||||
'analytic',
|
||||
'mail',
|
||||
'portal',
|
||||
'resource',
|
||||
'web_kanban',
|
||||
'web_planner',
|
||||
|
||||
@@ -65,14 +65,10 @@
|
||||
<field name="date_start" eval="time.strftime('%Y-%m-01 10:00:00')"/>
|
||||
<field name="name">Website for Sales & WMS</field>
|
||||
<field name="color">3</field>
|
||||
<field name="partner_id" ref="portal.partner_demo_portal"/>
|
||||
<field name="privacy_visibility">portal</field>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="alias_model">project.task</field>
|
||||
<field name="type_ids" eval="[(4, ref('project_stage_0')), (4, ref('project_stage_1')), (4, ref('project_stage_2')), (4, ref('project_stage_3'))]"/>
|
||||
</record>
|
||||
<function model="project.project" name="message_subscribe"
|
||||
eval="[ref('project_project_1')], [ref('portal.partner_demo_portal')]"/>
|
||||
|
||||
<record id="project_project_2" model="project.project">
|
||||
<field name="name">Research & Development</field>
|
||||
|
||||
@@ -104,14 +104,6 @@ class Project(models.Model):
|
||||
""" Overriden in project_issue to offer more options """
|
||||
return [('project.task', "Tasks")]
|
||||
|
||||
def _get_visibility_selection(self):
|
||||
""" Overriden in portal_project to offer more options """
|
||||
return [
|
||||
('employees', _('Visible by all employees')),
|
||||
('followers', _('On invitation only')),
|
||||
('portal', _('Shared with a customer'))
|
||||
]
|
||||
|
||||
@api.multi
|
||||
def attachment_tree_view(self):
|
||||
self.ensure_one()
|
||||
@@ -171,7 +163,6 @@ class Project(models.Model):
|
||||
|
||||
# Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
|
||||
_alias_models = lambda self: self._get_alias_models()
|
||||
_visibility_selection = lambda self: self._get_visibility_selection()
|
||||
|
||||
active = fields.Boolean(default=True,
|
||||
help="If the active field is set to False, it will allow you to hide the project without removing it.")
|
||||
@@ -203,15 +194,19 @@ class Project(models.Model):
|
||||
"with Tasks (or optionally Issues if the Issue Tracker module is installed).")
|
||||
alias_model = fields.Selection(_alias_models, string="Alias Model", index=True, required=True, default='project.task',
|
||||
help="The kind of document created when an email is received on this project's email alias")
|
||||
privacy_visibility = fields.Selection(_visibility_selection, string='Privacy', required=True,
|
||||
privacy_visibility = fields.Selection([
|
||||
('followers', _('On invitation only')),
|
||||
('employees', _('Visible by all employees')),
|
||||
('portal', _('Visible by following customers')),
|
||||
],
|
||||
string='Privacy', required=True,
|
||||
default='employees',
|
||||
help="Holds visibility of the tasks or issues that belong to the current project:\n"
|
||||
"- Portal : employees see everything;\n"
|
||||
" if portal is activated, portal users see the tasks or issues followed by\n"
|
||||
" them or by someone of their company\n"
|
||||
"- Employees Only: employees see all tasks or issues\n"
|
||||
"- Followers Only: employees see only the followed tasks or issues; if portal\n"
|
||||
" is activated, portal users see the followed tasks or issues.")
|
||||
"- On invitation only: Employees may only see the followed project, tasks or issues\n"
|
||||
"- Visible by all employees: Employees may see all project, tasks or issues\n"
|
||||
"- Visible by following customers: employees see everything;\n"
|
||||
" if website is activated, portal users may see project, tasks or issues followed by\n"
|
||||
" them or by someone of their company\n")
|
||||
doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached")
|
||||
date_start = fields.Date(string='Start Date')
|
||||
date = fields.Date(string='Expiration Date', index=True, track_visibility='onchange')
|
||||
|
||||
@@ -3,7 +3,6 @@ access_project_project,project.project,model_project_project,project.group_proje
|
||||
access_project_project_manager,project.project,model_project_project,project.group_project_manager,1,1,1,1
|
||||
access_account_analytic_account_user,account.analytic.account,analytic.model_account_analytic_account,project.group_project_user,1,0,0,0
|
||||
access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1
|
||||
access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0
|
||||
access_project_task_type_user,project.task.type.user,model_project_task_type,base.group_user,1,0,0,0
|
||||
access_project_task_type_project_user,project.task.type.project.user,model_project_task_type,project.group_project_user,1,0,0,0
|
||||
access_project_task_type_manager,project.task.type manager,model_project_task_type,project.group_project_manager,1,1,1,1
|
||||
@@ -21,8 +20,4 @@ access_resource_calendar_attendance,project.resource_calendar_attendance user,re
|
||||
access_resource_calendar_leaves_user,resource.calendar.leaves user,resource.model_resource_calendar_leaves,project.group_project_user,1,1,1,1
|
||||
access_project_tags_all,project.project_tags_all,model_project_tags,,1,0,0,0
|
||||
access_project_tags_manager,project.project_tags_manager,model_project_tags,project.group_project_manager,1,1,1,1
|
||||
access_project_tags_portal,project_tags_portal,project.model_project_tags,base.group_portal,1,0,0,0
|
||||
access_mail_alias,mail.alias,mail.model_mail_alias,project.group_project_manager,1,1,1,1
|
||||
access_project_portal,project__portal,project.model_project_project,base.group_portal,1,0,0,0
|
||||
access_task_portal,task_portal,project.model_project_task,base.group_portal,1,0,0,0
|
||||
access_task_type_portal,task_type_portal,project.model_project_task_type,base.group_portal,1,0,0,0
|
||||
|
||||
|
@@ -43,10 +43,10 @@
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="project_public_members_rule">
|
||||
<field name="name">Project: employees: portal, employees or followers</field>
|
||||
<field name="name">Project: employees: following required for follower-only projects</field>
|
||||
<field name="model_id" ref="model_project_project"/>
|
||||
<field name="domain_force">['|',
|
||||
('privacy_visibility', 'in', ['portal', 'employees']),
|
||||
('privacy_visibility', '!=', 'followers'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
@@ -63,15 +63,13 @@
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="task_visibility_rule">
|
||||
<field name="name">Project/Task: employees: portal or employee or (followers and following)</field>
|
||||
<field name="name">Project/Task: employees: follow required for follower-only projects</field>
|
||||
<field name="model_id" ref="model_project_task"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
('project_id.privacy_visibility', 'in', ['portal', 'employees']),
|
||||
('project_id.privacy_visibility', '!=', 'followers'),
|
||||
'|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
'|',
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
# to subscribe check access to the record, follower is not enough at creation
|
||||
@@ -87,42 +85,6 @@
|
||||
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Portal -->
|
||||
<record model="ir.rule" id="portal_project_rule">
|
||||
<field name="name">Project: portal users: portal or following</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
'&',
|
||||
('privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('privacy_visibility', '=', 'followers'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="portal_task_rule">
|
||||
<field name="name">Project/Task: portal users: (portal and colleagues following) or (followers and following)</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]),
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
'&',
|
||||
# on employee project can receive messages but not access the object
|
||||
('project_id.privacy_visibility', '!=', 'employees'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id])
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.rule" id="report_project_task_user_report_comp_rule">
|
||||
<field name="name">Task Analysis multi-company</field>
|
||||
<field name="model_id" ref="model_report_project_task_user"/>
|
||||
|
||||
@@ -31,37 +31,6 @@ class TestPortalProjectBase(TestProjectBase):
|
||||
|
||||
class TestPortalProject(TestPortalProjectBase):
|
||||
|
||||
@mute_logger('openerp.addons.base.ir.ir_model')
|
||||
def test_portal_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
pigs.write({'privacy_visibility': 'portal'})
|
||||
|
||||
# Do: Alfred reads project -> ok (employee ok public)
|
||||
pigs.sudo(self.user_projectuser).read(['user_id'])
|
||||
# Test: all project tasks visible
|
||||
tasks = self.env['project.task'].sudo(self.user_projectuser).search([('project_id', '=', pigs.id)])
|
||||
self.assertEqual(tasks, self.task_1 | self.task_2 | self.task_3 | self.task_4 | self.task_5 | self.task_6,
|
||||
'access rights: project user should see all tasks of a portal project')
|
||||
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
|
||||
# Test: no project task searchable
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_noone).search, [('project_id', '=', pigs.id)])
|
||||
|
||||
# Data: task follower
|
||||
pigs.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id])
|
||||
self.task_1.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
|
||||
self.task_3.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
|
||||
# Do: Chell reads project -> ok (portal ok public)
|
||||
pigs.sudo(self.user_portal).read(['user_id'])
|
||||
# Do: Donovan reads project -> ko (public ko portal)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Test: no access right to project.task
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_public).search, [])
|
||||
# Data: task follower cleaning
|
||||
self.task_1.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
|
||||
self.task_3.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
|
||||
|
||||
@mute_logger('openerp.addons.base.ir.ir_model')
|
||||
def test_employee_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
@@ -76,11 +45,6 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
'access rights: project user cannot see all tasks of an employees project')
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
|
||||
# Do: Chell reads project -> ko (portal ko employee)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_portal).read, ['user_id'])
|
||||
# Test: no project task visible + assigned
|
||||
tasks = self.env['project.task'].sudo(self.user_portal).search([('project_id', '=', pigs.id)])
|
||||
self.assertFalse(tasks.ids, 'access rights: portal user should not see tasks of an employees project, even if assigned')
|
||||
# Do: Donovan reads project -> ko (public ko employee)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Do: project user is employee and can create a task
|
||||
@@ -104,27 +68,16 @@ class TestPortalProject(TestPortalProjectBase):
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
|
||||
|
||||
# Do: Chell reads project -> ko (portal ko employee)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_portal).read, ['user_id'])
|
||||
# Test: no project task visible
|
||||
tasks = self.env['project.task'].sudo(self.user_portal).search([('project_id', '=', pigs.id)])
|
||||
self.assertEqual(tasks, self.task_3,
|
||||
'access rights: portal user should not see tasks of a not-followed followers project, only assigned')
|
||||
|
||||
# Do: Donovan reads project -> ko (public ko employee)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
|
||||
# Data: subscribe Alfred, Chell and Donovan as follower
|
||||
pigs.message_subscribe_users(user_ids=[self.user_projectuser.id, self.user_portal.id, self.user_public.id])
|
||||
self.task_1.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id, self.user_projectuser.id])
|
||||
self.task_3.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id, self.user_projectuser.id])
|
||||
pigs.message_subscribe_users(user_ids=[self.user_projectuser.id])
|
||||
|
||||
# Do: Alfred reads project -> ok (follower ok followers)
|
||||
prout = pigs.sudo(self.user_projectuser)
|
||||
prout.invalidate_cache()
|
||||
prout.read(['user_id'])
|
||||
# Do: Chell reads project -> ok (follower ok follower)
|
||||
pigs.sudo(self.user_portal).read(['user_id'])
|
||||
|
||||
# Do: Donovan reads project -> ko (public ko follower even if follower)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Do: project user is follower of the project and can create a task
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestProjectBase(TestMail):
|
||||
# Test 'Pigs' project
|
||||
cls.project_pigs = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Pigs',
|
||||
'privacy_visibility': 'portal',
|
||||
'privacy_visibility': 'employees',
|
||||
'alias_name': 'project+pigs',
|
||||
'partner_id': cls.partner_1.id})
|
||||
# Already-existing tasks in Pigs
|
||||
@@ -48,7 +48,7 @@ class TestProjectBase(TestMail):
|
||||
# Test 'Goats' project, same as 'Pigs', but with 2 stages
|
||||
cls.project_goats = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Goats',
|
||||
'privacy_visibility': 'portal',
|
||||
'privacy_visibility': 'followers',
|
||||
'alias_name': 'project+goats',
|
||||
'partner_id': cls.partner_1.id,
|
||||
'type_ids': [
|
||||
|
||||
@@ -107,8 +107,7 @@
|
||||
<page string="Settings">
|
||||
<group>
|
||||
<field name="privacy_visibility" widget="radio"/>
|
||||
<field name="partner_id" string="Customer"
|
||||
attrs="{'invisible':[('privacy_visibility','in',['employees', 'followers'])]}"/>
|
||||
<field name="partner_id" string="Customer"/>
|
||||
<field name="user_id" string="Project Manager"
|
||||
attrs="{'readonly':[('active','=',False)]}"
|
||||
groups="base.group_no_one"/>
|
||||
@@ -744,10 +743,6 @@
|
||||
</record>
|
||||
<menuitem action="project_tags_action" id="menu_project_tags_act" parent="menu_project_config" groups="base.group_no_one"/>
|
||||
|
||||
<!-- Portal menuitems -->
|
||||
<menuitem name="Projects" id="portal_services_projects" parent="portal.portal_projects"
|
||||
action="open_view_project_all" sequence="10"/>
|
||||
|
||||
<!-- Reporting menus -->
|
||||
<menuitem id="base.menu_project_report" name="Reports"
|
||||
groups="project.group_project_manager"
|
||||
|
||||
@@ -12,5 +12,4 @@ Allows your customers to manage their account from a beautiful web interface.
|
||||
'data': [
|
||||
'views/templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</t>
|
||||
<t t-if="group['date_begin'] != date">
|
||||
<li>
|
||||
<a t-ignore="True" t-attf-href="#{default_url}?date_begin=#{group['date_begin']}&date_end=#{group['date_end']}"><t t-esc="group['name']"/><span class="pull-right badge" t-esc="group['item_count']"/></a>
|
||||
<a t-ignore="True" t-attf-href="#{default_url}?#{keep_query()}&date_begin=#{group['date_begin']}&date_end=#{group['date_end']}"><t t-esc="group['name']"/><span class="pull-right badge" t-esc="group['item_count']"/></a>
|
||||
</li>
|
||||
</t>
|
||||
</t>
|
||||
@@ -77,7 +77,7 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="website_portal.custom_panel" inherit_id='website_portal.portal_layout' customize_show="True" name="Editable Panel" active="False">
|
||||
<template id="custom_panel" inherit_id="portal_layout" customize_show="True" name="Portal Editable Panel" active="False">
|
||||
<xpath expr="//div[@id='o_my_sidebar']" position="inside">
|
||||
<div class="oe_structure">
|
||||
<h3 class="page-header">Custom Panel</h3>
|
||||
@@ -197,4 +197,5 @@
|
||||
<li><a href="/my/home" role="menuitem">My Account</a></li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import controllers
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': "Website Project",
|
||||
'description': """
|
||||
Website portal for Project and Tasks
|
||||
====================================
|
||||
""",
|
||||
'category': 'Website',
|
||||
'depends': ['project', 'website_portal'],
|
||||
'data': [
|
||||
'security/project_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/project_templates.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/project_demo.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import main
|
||||
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.website_portal.controllers.main import website_account
|
||||
|
||||
|
||||
class WebsiteAccount(website_account):
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(WebsiteAccount, self)._prepare_portal_layout_values()
|
||||
project_count = request.env['project.project'].search_count([('privacy_visibility','=','portal')])
|
||||
task_count = request.env['project.task'].search_count([('project_id.privacy_visibility','=','portal')])
|
||||
values.update({
|
||||
'project_count': project_count,
|
||||
'task_count': task_count,
|
||||
})
|
||||
return values
|
||||
|
||||
@http.route(['/my/projects', '/my/projects/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def my_projects(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
Project = request.env['project.project']
|
||||
|
||||
sortings = {
|
||||
'date': {'label': _('Newest'), 'order': 'create_date desc'},
|
||||
'name': {'label': _('Name'), 'order': 'name'},
|
||||
}
|
||||
|
||||
domain = [('privacy_visibility','=','portal')]
|
||||
order = sortings.get(sortby, sortings['date'])['order']
|
||||
|
||||
# archive groups - Default Group By 'create_date'
|
||||
archive_groups = self._get_archive_groups('project.project', domain)
|
||||
if date_begin and date_end:
|
||||
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||
# pager
|
||||
pager = request.website.pager(
|
||||
url="/my/projects",
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
|
||||
total=values['project_count'],
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
)
|
||||
|
||||
# content according to pager and archive selected
|
||||
projects = Project.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
|
||||
|
||||
values.update({
|
||||
'date': date_begin,
|
||||
'date_end': date_end,
|
||||
'sortings': sortings,
|
||||
'sortby': sortby,
|
||||
'projects': projects,
|
||||
'page_name': 'project',
|
||||
'archive_groups': archive_groups,
|
||||
'default_url': '/my/projects',
|
||||
'pager': pager
|
||||
})
|
||||
return request.website.render("website_project.my_projects", values)
|
||||
|
||||
@http.route(['/my/project/<model("project.project"):project>'], type='http', auth="user", website=True)
|
||||
def my_project(self, project=None, **kw):
|
||||
return request.website.render("website_project.my_project", {'project': project})
|
||||
|
||||
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def my_tasks(self, page=1, date_begin=None, date_end=None, project=None, sortby=None, **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
|
||||
sortings = {
|
||||
'date': {'label': _('Newest'), 'order': 'create_date desc'},
|
||||
'name': {'label': _('Name'), 'order': 'name'},
|
||||
'stage': {'label': _('Stage'), 'order': 'stage_id'},
|
||||
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc'},
|
||||
}
|
||||
|
||||
projects = request.env['project.project'].search([('privacy_visibility', '=', 'portal')])
|
||||
|
||||
project_filters = {
|
||||
'all': {'label': _('All'), 'domain': []},
|
||||
}
|
||||
|
||||
for proj in projects:
|
||||
project_filters.update({
|
||||
str(proj.id): {'label': proj.name, 'domain': [('project_id', '=', proj.id)]}
|
||||
})
|
||||
|
||||
domain = [('project_id.privacy_visibility', '=', 'portal')]
|
||||
domain += project_filters.get(project, project_filters['all'])['domain']
|
||||
order = sortings.get(sortby, sortings['date'])['order']
|
||||
|
||||
# archive groups - Default Group By 'create_date'
|
||||
archive_groups = self._get_archive_groups('project.task', domain)
|
||||
if date_begin and date_end:
|
||||
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||
|
||||
# pager
|
||||
pager = request.website.pager(
|
||||
url="/my/tasks",
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'project': project},
|
||||
total=values['task_count'],
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
)
|
||||
# content according to pager and archive selected
|
||||
tasks = request.env['project.task'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
|
||||
|
||||
values.update({
|
||||
'date': date_begin,
|
||||
'date_end': date_end,
|
||||
'project_filters': OrderedDict(sorted(project_filters.items())),
|
||||
'projects': projects,
|
||||
'project': project,
|
||||
'sortings': sortings,
|
||||
'sortby': sortby,
|
||||
'tasks': tasks,
|
||||
'page_name': 'task',
|
||||
'archive_groups': archive_groups,
|
||||
'default_url': '/my/tasks',
|
||||
'pager': pager
|
||||
})
|
||||
return request.website.render("website_project.my_tasks", values)
|
||||
|
||||
@http.route(['/my/task/<model("project.task"):task>'], type='http', auth="user", website=True)
|
||||
def my_task(self, task=None, **kw):
|
||||
return request.website.render("website_project.my_task", {'task': task, 'user': request.env.user})
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="project.project_project_1" model="project.project">
|
||||
<field name="partner_id" ref="portal.partner_demo_portal"/>
|
||||
<field name="privacy_visibility">portal</field>
|
||||
</record>
|
||||
|
||||
<function model="project.project" name="message_subscribe"
|
||||
eval="[ref('project.project_project_1')], [ref('portal.partner_demo_portal')]"/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0
|
||||
access_project_tags_portal,project_tags_portal,project.model_project_tags,base.group_portal,1,0,0,0
|
||||
access_project_portal,project__portal,project.model_project_project,base.group_portal,1,0,0,0
|
||||
access_task_portal,task_portal,project.model_project_task,base.group_portal,1,0,0,0
|
||||
access_task_type_portal,task_type_portal,project.model_project_task_type,base.group_portal,1,0,0,0
|
||||
|
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Portal -->
|
||||
<record id="project.portal_project_rule" model="ir.rule">
|
||||
<field name="name">Project: portal users: portal and following</field>
|
||||
<field name="model_id" ref="project.model_project_project"/>
|
||||
<field name="domain_force">[
|
||||
'&',
|
||||
('privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project.portal_task_rule" model="ir.rule">
|
||||
<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.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]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import test_access_rights
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.addons.project.tests.test_access_rights import TestPortalProjectBase
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestPortalProject(TestPortalProjectBase):
|
||||
@mute_logger('openerp.addons.base.ir.ir_model')
|
||||
def test_portal_project_access_rights(self):
|
||||
pigs = self.project_pigs
|
||||
pigs.write({'privacy_visibility': 'portal'})
|
||||
|
||||
# Do: Alfred reads project -> ok (employee ok public)
|
||||
pigs.sudo(self.user_projectuser).read(['user_id'])
|
||||
# Test: all project tasks visible
|
||||
tasks = self.env['project.task'].sudo(self.user_projectuser).search([('project_id', '=', pigs.id)])
|
||||
self.assertEqual(tasks, self.task_1 | self.task_2 | self.task_3 | self.task_4 | self.task_5 | self.task_6,
|
||||
'access rights: project user should see all tasks of a portal project')
|
||||
|
||||
# Do: Bert reads project -> crash, no group
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
|
||||
# Test: no project task searchable
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_noone).search, [('project_id', '=', pigs.id)])
|
||||
|
||||
# Data: task follower
|
||||
pigs.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id])
|
||||
self.task_1.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
|
||||
self.task_3.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
|
||||
# Do: Chell reads project -> ok (portal ok public)
|
||||
pigs.sudo(self.user_portal).read(['user_id'])
|
||||
# Do: Donovan reads project -> ko (public ko portal)
|
||||
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
|
||||
# Test: no access right to project.task
|
||||
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_public).search, [])
|
||||
# Data: task follower cleaning
|
||||
self.task_1.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
|
||||
self.task_3.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
|
||||
@@ -0,0 +1,295 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="portal_layout" name="Portal layout: project menu entry" inherit_id="website_portal.portal_layout" priority="28">
|
||||
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
|
||||
<li t-if="project_count" t-att-class="page_name == 'project' and 'active' or ''">
|
||||
<a href="/my/projects">Projects</a>
|
||||
</li>
|
||||
<li t-if="task_count" t-att-class="page_name == 'task' and 'active' or ''">
|
||||
<a href="/my/tasks">Tasks</a>
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_home" name="Portal My Home: project entries" inherit_id="website_portal.portal_my_home" priority="28">
|
||||
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
|
||||
<h3 t-if="project_count" class="page-header">
|
||||
<a href="/my/projects">Your Projects
|
||||
<small class="ml8">
|
||||
<span class='badge'><t t-esc="project_count"/></span>
|
||||
</small>
|
||||
</a>
|
||||
</h3>
|
||||
<h3 t-if="task_count" class="page-header">
|
||||
<a href="/my/tasks">Your Tasks
|
||||
<small class="ml8">
|
||||
<span class='badge'><t t-esc="task_count"/></span>
|
||||
</small>
|
||||
</a>
|
||||
</h3>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="my_projects" name="My Projects">
|
||||
<t t-call="website_portal.portal_layout">
|
||||
<div class="row">
|
||||
<h3 class="page-header">
|
||||
Your Projects
|
||||
<div class="dropdown pull-right mr8">
|
||||
<button id="sortby" class="btn btn-default" type="button" data-toggle="dropdown">
|
||||
<span class="fa fa-sort fa-lg" /> <span t-esc="sortings.get(sortby, {}).get('label', 'Newest')"/>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="sortby">
|
||||
<li t-foreach="sortings" t-as="option" t-attf-class="#{sortby == option and 'active'}">
|
||||
<a t-att-href="default_url + '?' + keep_query('date_begin', 'date_end', sortby=option)"><span t-esc="sortings.get(option).get('label')"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<t t-if="not projects">
|
||||
<div class="alert alert-warning mt8" role="alert">
|
||||
There are no projects.
|
||||
</div>
|
||||
</t>
|
||||
<table t-if="projects" class="table table-hover status_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th class="col-md-6">Name</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="projects" t-as="project">
|
||||
<td>
|
||||
<a t-attf-href="/my/project/#{project.id}?{{ keep_query() }}"><span t-field="project.name"/></a>
|
||||
</td>
|
||||
<td>
|
||||
<a t-if="project.use_tasks" t-attf-href="/my/tasks?project=#{project.id}">
|
||||
<t t-esc="project.task_count" />
|
||||
<t t-esc="project.label_tasks" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="website.pager"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="my_project" name="My Project">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="container">
|
||||
<div class="oe_structure">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<ol class="breadcrumb mt8">
|
||||
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
|
||||
<li><a t-attf-href="/my/projects?#{keep_query()}">My Projects</a></li>
|
||||
<li><span t-field="project.name"/></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>
|
||||
Project - <span t-field="project.name"/>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class='row'>
|
||||
<div class="col-md-6">
|
||||
<t t-if="project.partner_id">
|
||||
<strong>Customer</strong>
|
||||
<div t-if="project.partner_id">
|
||||
<address t-field="project.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="project.user_id">
|
||||
<strong>Project Manager</strong>
|
||||
<div>
|
||||
<address t-field="project.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="pull-right">
|
||||
<a t-attf-href="/my/tasks?project=#{project.id}" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-tasks" />
|
||||
<span t-esc="project.task_count" />
|
||||
<span t-field="project.label_tasks" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32">
|
||||
<div class="col-md-12">
|
||||
<h4><strong>Message and communication history</strong></h4>
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1 mt16">
|
||||
<t t-call="website_mail.message_thread">
|
||||
<t t-set="object" t-value="project"/>
|
||||
<t t-set="chatter_mode" t-value="'json'"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="my_tasks" name="My Tasks">
|
||||
<t t-call="website_portal.portal_layout">
|
||||
<h3 class="page-header">
|
||||
Your Tasks
|
||||
<div class="dropdown pull-right mr8">
|
||||
<button id="sortby" class="btn btn-default" type="button" data-toggle="dropdown">
|
||||
<span class="fa fa-sort fa-lg" /> <span t-esc="sortings.get(sortby, {}).get('label', 'Newest')"/>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="sortby">
|
||||
<li t-foreach="sortings" t-as="option" t-att-class="sortby == option and 'active'">
|
||||
<a t-attf-href="/my/tasks?{{ keep_query('date_begin', 'date_end', 'project', sortby=option) }}"><span t-esc="sortings.get(option).get('label')"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown pull-right mr8">
|
||||
<button id="project_filters" class="btn btn-default" type="button" data-toggle="dropdown">
|
||||
Projects: <span t-esc="project_filters.get(project, {}).get('label', 'All')"/>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="project_filters">
|
||||
<li t-foreach="project_filters" t-as="option" t-att-class="project == option and 'active'">
|
||||
<a t-attf-href="/my/tasks?{{ keep_query('date_begin', 'date_end', 'sortby', project=option) }}"><span t-esc="project_filters.get(option).get('label')"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="panel panel-default">
|
||||
<t t-if="not tasks">
|
||||
<div class="alert alert-warning mt8" role="alert">
|
||||
There are no tasks.
|
||||
</div>
|
||||
</t>
|
||||
<table t-if="tasks" class="table table-hover status_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th class="col-md-10">Task</th>
|
||||
<th>Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="tasks" t-as="task">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="/my/task/#{task.id}?{{ keep_query() }}"><span t-field="task.name"/></a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label label-info" title="Current stage of the task" t-esc="task.stage_id.name" />
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="website.pager"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="my_task" name="My Task">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="container">
|
||||
<div class="oe_structure">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb mt8">
|
||||
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
|
||||
<li><a t-attf-href="/my/tasks?#{keep_query()}">My Tasks</a></li>
|
||||
<li><span t-field="task.name"/></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>
|
||||
Task - <span t-field="task.name"/>
|
||||
<span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/>
|
||||
<a class="btn btn-info" t-att-href="'/web#return_label=Website&model=project.task&id=%s&view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="mb8" t-if="user.partner_id.id in task.sudo().project_id.message_partner_ids.ids">
|
||||
<strong>Project:</strong> <a t-attf-href="/my/project/#{task.project_id.id}" t-field="task.project_id.name"/>
|
||||
</div>
|
||||
|
||||
<div class='row'>
|
||||
<div class="col-md-6">
|
||||
<div class="mb8">
|
||||
<strong>Date:</strong> <span t-field="task.create_date" t-field-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
|
||||
<div t-if="task.user_id">
|
||||
<strong>Assigned to</strong>
|
||||
<div>
|
||||
<address t-field="task.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb8" t-if="task.date_deadline">
|
||||
<strong>Deadline:</strong> <span t-field="task.date_deadline" t-field-options='{"widget": "date"}'/>
|
||||
</div>
|
||||
<div t-if="task.partner_id">
|
||||
<strong>Reported by</strong>
|
||||
<div>
|
||||
<address t-field="task.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" t-if="task.description">
|
||||
<p t-field="task.description" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt32">
|
||||
<div class="col-md-12">
|
||||
<h4><strong>Message and communication history</strong></h4>
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1 mt16">
|
||||
<t t-call="website_mail.message_thread">
|
||||
<t t-set="object" t-value="task"/>
|
||||
<t t-set="chatter_mode" t-value="'json'"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user