[MERGE] portal website_project: portal from backend to frontend.

Move the portal features of project to website_project to display tasks in the
website portal.

Add website_project_timesheet to display timesheets on website_project tasks.

Remove all menuitems related to the backend portal as the frontend portal now
provides all the models.

Original authors: Vipul Bhatt, Florian Wintjens
This commit is contained in:
Antony Lesuisse
2016-07-06 01:26:15 +02:00
36 changed files with 788 additions and 401 deletions
-14
View File
@@ -1,19 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Top menu item -->
<menuitem name="Portal"
id="portal_menu"
groups="base.group_portal"
web_icon="portal,static/description/icon.png"
sequence="15"/>
<!--
Create menu items that we'll leave empty for now - they'll be
filled up by other portal modules.
-->
<menuitem name="Billing" id="portal_orders" parent="portal_menu" sequence="20"/>
<menuitem name="After Sale Services" id="portal_after_sales" parent="portal_menu" sequence="30"/>
<menuitem name="Projects" id="portal_projects" parent="portal_menu" sequence="40"/>
<!-- extend res.groups search view -->
<record id="group_search_view" model="ir.ui.view">
@@ -12,120 +12,4 @@
</field>
</record>
<record id="view_order_form_portal_sale" model="ir.ui.view">
<field name="name">sale.order.form.portal_sale</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="groups_id" eval="[(4, ref('base.group_portal'))]"/>
<field name="arch" type="xml">
<data>
<header position="replace">
<field name="state" invisible="1"/>
</header>
<xpath expr='//field[@name="order_line"]/form//field[@name="procurement_ids"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="product_id"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered_updateable"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//label[@for="qty_delivered"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_delivered"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//label[@for="qty_invoiced"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="qty_invoiced"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/form//field[@name="price_unit"]' position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
<xpath expr='//field[@name="order_line"]/form//field[@name="tax_id"]' position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
<xpath expr='//field[@name="order_line"]/tree//field[@name="procurement_ids"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/tree//field[@name="product_id"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/tree//field[@name="qty_delivered_updateable"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/tree//field[@name="qty_delivered"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/tree//field[@name="qty_invoiced"]' position="replace"/>
<xpath expr='//field[@name="order_line"]/tree//field[@name="price_unit"]' position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
<xpath expr='//field[@name="order_line"]/tree//field[@name="tax_id"]' position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
<xpath expr='//field[@name="order_line"]/kanban//field[@name="product_id"]' position="replace">
<field name="name"/>
</xpath>
<xpath expr='//field[@name="order_line"]/kanban/templates/t/div//span/t' position="replace">
<t t-esc="record.name.value"/>
</xpath>
</data>
</field>
</record>
<record id="invoice_form_portal" model="ir.ui.view">
<field name="name">account.invoice.form</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="groups_id" eval="[(4, ref('base.group_portal'))]"/>
<field name="arch" type="xml">
<data>
<xpath expr='//div[contains(@attrs, "has_outstanding")]' position="replace"/>
<field name="has_outstanding" position="replace"/>
<field name="payments_widget" position="replace"/>
<field name="outstanding_credits_debits_widget" position="replace"/>
</data>
</field>
</record>
<!--
Override the original action to set another help field and/or
another context field, more suited for portal members
-->
<record id="action_quotations_portal" model="ir.actions.act_window">
<field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('state','in',('draft','sent','cancel'))]</field>
<field name="context">{}</field>
<field name="search_view_id" ref="sale.sale_order_view_search_inherit_quotation"/>
<field name="help">We haven't sent you any quotation.</field>
</record>
<record id="action_orders_portal" model="ir.actions.act_window">
<field name="name">Sale Orders</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="sale.sale_order_view_search_inherit_sale"/>
<field name="domain">[('state', 'not in', ('draft', 'sent', 'cancel'))]</field>
<field name="context">{}</field>
<field name="help">We haven't sent you any sales order.</field>
</record>
<record id="portal_action_invoices" model="ir.actions.act_window">
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('type','in',['out_invoice','out_refund'])]</field>
<field name="context">{}</field>
<field name="search_view_id" ref="account.view_account_invoice_filter"/>
<field name="help">We haven't sent you any invoice.</field>
</record>
<record id="portal_action_invoices_tree_spec" model="ir.actions.act_window.view">
<field name="act_window_id" ref="portal_action_invoices"/>
<field name="view_id" ref="account.invoice_tree"/>
<field name="view_mode">tree</field>
<field name="sequence" eval="0"/>
</record>
<record id="portal_action_invoices_form_spec" model="ir.actions.act_window.view">
<field name="act_window_id" ref="portal_action_invoices"/>
<field name="view_id" ref="account.invoice_form"/>
<field name="view_mode">form</field>
<field name="sequence" eval="1"/>
</record>
<menuitem name="Quotations" id="portal_quotations" parent="portal.portal_orders"
action="action_quotations_portal" sequence="10"/>
<menuitem name="Sales Orders" id="portal_sales_orders" parent="portal.portal_orders"
action="action_orders_portal" sequence="20"/>
<menuitem name="Invoices" id="portal_invoices" parent="portal.portal_orders"
action="portal_action_invoices" sequence="30"/>
</odoo>
-1
View File
@@ -13,7 +13,6 @@
'product',
'analytic',
'mail',
'portal',
'resource',
'web_kanban',
'web_planner',
-4
View File
@@ -65,14 +65,10 @@
<field name="date_start" eval="time.strftime('%Y-%m-01 10:00:00')"/>
<field name="name">Website for Sales &amp; 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 &amp; Development</field>
+11 -16
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_project_project_manager project.project model_project_project project.group_project_manager 1 1 1 1
4 access_account_analytic_account_user account.analytic.account analytic.model_account_analytic_account project.group_project_user 1 0 0 0
5 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
6 access_project_task_type_user project.task.type.user model_project_task_type base.group_user 1 0 0 0
7 access_project_task_type_project_user project.task.type.project.user model_project_task_type project.group_project_user 1 0 0 0
8 access_project_task_type_manager project.task.type manager model_project_task_type project.group_project_manager 1 1 1 1
20 access_resource_calendar_leaves_user resource.calendar.leaves user resource.model_resource_calendar_leaves project.group_project_user 1 1 1 1
21 access_project_tags_all project.project_tags_all model_project_tags 1 0 0 0
22 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
23 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
+5 -43
View File
@@ -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'),
'|',
'&amp;',
('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">[
'|',
'&amp;',
('privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('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">[
'|',
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
'&amp;',
# 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"/>
+2 -49
View File
@@ -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
+2 -2
View File
@@ -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': [
+1 -6
View File
@@ -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"
@@ -19,8 +19,10 @@ class Project(models.Model):
res.append(("project.issue", "Issues"))
return res
@api.multi
def _compute_issue_count(self):
self.issue_count = self.env['project.issue'].search_count([('project_id', '=', self.id), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
for project in self:
project.issue_count = self.env['project.issue'].search_count([('project_id', '=', project.id), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
def _issue_needaction_count(self):
issue_data = self.env['project.issue'].read_group([('project_id', 'in', self.ids), ('message_needaction', '=', True)], ['project_id'], ['project_id'])
@@ -10,40 +10,16 @@
</record>
<record id="issue_user_rule" model="ir.rule">
<field name="name">Project/Issue: employees: employee or (followers and following)</field>
<field name="name">Project/Issue: employees: no need to follow or (following project or following issue</field>
<field name="model_id" ref="model_project_issue"/>
<field name="domain_force">['|',
('project_id.privacy_visibility', '=', 'employees'),
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('project_id.privacy_visibility', '!=', 'followers'),
'|',
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('message_partner_ids', 'in', [user.partner_id.id]),
]</field>
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
</record>
<record id="portal_issue_rule" model="ir.rule">
<field name="name">Project/Issue: portal users: (portal and colleagues following) or (followers and following)</field>
<field name="model_id" ref="project_issue.model_project_issue"/>
<field name="domain_force">['|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_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="project_issue.issue_user_rule">
<field name="name">Project/Issue: employees: portal, employee or (followers and following)</field>
<field name="domain_force">['|',
('project_id.privacy_visibility', 'in', ['portal', 'employees']),
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('message_partner_ids', 'in', [user.partner_id.id]),
]</field>
</record>
</data>
</odoo>
@@ -267,26 +267,4 @@
</p>
</field>
</record>
<!-- Portal -->
<record id="open_view_issue" model="ir.actions.act_window">
<field name="name">Issues</field>
<field name="res_model">project.issue</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="project_issue.project_issue_tree_view"/>
<field name="domain" eval=""/>
<field name="context">{"search_default_user_id":'', "search_default_draft":'', "search_default_todo":'', "portal":'True'}</field>
<field name="search_view_id" ref="project_issue.view_project_issue_filter"/>
<field name="target">current</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create an issue.
</p><p>
You can track your issues from this menu and the action we will take.
</p>
</field>
</record>
<menuitem id="portal_services_issues" name="Issues" parent="portal.portal_projects"
action="open_view_issue" sequence="10"/>
</odoo>
-1
View File
@@ -12,5 +12,4 @@ Allows your customers to manage their account from a beautiful web interface.
'data': [
'views/templates.xml',
],
'installable': True,
}
+3 -2
View File
@@ -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']}&amp;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()}&amp;date_begin=#{group['date_begin']}&amp;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>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
+20
View File
@@ -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
+130
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_analytic_account_portal account_analytic_account analytic.model_account_analytic_account base.group_portal 1 0 0 0
3 access_project_tags_portal project_tags_portal project.model_project_tags base.group_portal 1 0 0 0
4 access_project_portal project__portal project.model_project_project base.group_portal 1 0 0 0
5 access_task_portal task_portal project.model_project_task base.group_portal 1 0 0 0
6 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">[
'&amp;',
('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">[
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('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>
+4
View File
@@ -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&amp;model=project.task&amp;id=%s&amp;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>
+2 -5
View File
@@ -1,22 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Website Project Issue',
'category': 'Hidden',
'version': '0.1',
'complexity': 'easy',
'description': """
This module adds project issues inside your account's page on website if project_issue and website_portal are installed.
==================================================================================================
""",
'depends': ['project_issue', 'website_portal'],
'depends': ['project_issue', 'website_project'],
'data': [
'security/portal_security.xml',
'security/ir.model.access.csv',
'views/project_issue_templates.xml',
],
'installable': True,
'auto_install': True,
'category': 'Hidden',
}
@@ -1,58 +1,83 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import http
from collections import OrderedDict
from openerp import http, _
from openerp.addons.website_portal.controllers.main import website_account
from openerp.http import request
class WebsiteAccount(website_account):
@http.route()
def account(self):
response = super(WebsiteAccount, self).account()
# TDE FIXME: shouldn't that be mnaged by the access rule itself ?
# portal projects where you or someone from your company are a follower
user = request.env.user
issue_count = request.env['project.issue'].sudo().search_count([
'&',
('project_id.privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id, user.partner_id.id])
])
response.qcontext.update({'issue_count': issue_count})
return response
def _prepare_portal_layout_values(self):
values = super(WebsiteAccount, self)._prepare_portal_layout_values()
# domain is needed to hide non portal project for employee
issue_count = request.env['project.issue'].search_count([('project_id.privacy_visibility','=','portal')])
values.update({
'issue_count': issue_count,
})
return values
@http.route(['/my/issues', '/my/issues/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_issues(self, page=1, date_begin=None, date_end=None, **kw):
def my_issues(self, page=1, date_begin=None, date_end=None, project=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
ProjectIssue = request.env['project.issue']
domain = []
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.issue', domain)
if date_begin and date_end:
domain += [('create_date', '>=', date_begin), ('create_date', '<', date_end)]
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# pager
issue_count = ProjectIssue.search_count(domain)
pager = request.website.pager(
url="/my/issues",
url_args={'date_begin': date_begin, 'date_end': date_end},
total=issue_count,
total=values['issue_count'],
page=page,
step=self._items_per_page
)
# content according to pager and archive selected
project_issues = ProjectIssue.search(domain, order="stage_id", limit=self._items_per_page, offset=pager['offset'])
project_issues = request.env['project.issue'].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,
'issues': project_issues,
'page_name': 'issue',
'archive_groups': archive_groups,
'default_url': '/my/issues',
'pager': pager
})
return request.website.render("website_project_issue.portal_my_issues", values)
return request.website.render("website_project_issue.my_issues", values)
@http.route(['/my/issues/<int:issue_id>'], type='http', auth="user", website=True)
def issues_followup(self, issue_id=None, **kw):
def my_issues_issue(self, issue_id=None, **kw):
issue = request.env['project.issue'].browse(issue_id)
return request.website.render("website_project_issue.issues_followup", {'issue': issue})
return request.website.render("website_project_issue.my_issues_issue", {'issue': issue})
@@ -1,29 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<odoo>
<data noupdate="1">
<record id="portal_issue_rule" model="ir.rule">
<field name="name">Project/Issue: portal users: (portal and colleagues following) or (followers and following)</field>
<field name="model_id" ref="project_issue.model_project_issue"/>
<field name="domain_force">['|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('message_partner_ids', 'in', [user.partner_id.id]),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="portal_issue_rule" model="ir.rule">
<field name="name">Project/Issue: portal users: (portal and (following project or following issue)</field>
<field name="model_id" ref="project_issue.model_project_issue"/>
<field name="domain_force">[
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('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>
<record model="ir.rule" id="project_issue.issue_user_rule">
<field name="name">Project/Issue: employees: portal, employee or (followers and following)</field>
<field name="domain_force">['|',
('project_id.privacy_visibility', 'in', ['portal', 'employees']),
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('message_partner_ids', 'in', [user.partner_id.id]),
]</field>
</record>
</data>
</openerp>
</data>
</odoo>
@@ -84,18 +84,6 @@ class TestPortalIssue(TestPortalProjectBase):
# ----------------------------------------
self.project_pigs.write({'privacy_visibility': 'followers'})
# Do: Alfred reads project -> ko (employee ko followers)
# Test: no project issue visible
issues = Issue.sudo(self.user_projectuser.id).search([('project_id', '=', pigs_id)])
self.assertEqual(set(issues.ids), set([self.issue_4.id]),
'access rights: employee user should not see issues of a not-followed followers project, only assigned')
# Do: Chell reads project -> ko (portal ko employee)
# Test: no project issue visible
issues = Issue.sudo(self.user_portal.id).search([('project_id', '=', pigs_id)])
self.assertEqual(set(issues.ids), set([self.issue_5.id]),
'access rights: portal user should not see issues of a not-followed followers project, only assigned')
# Data: subscribe Alfred, Chell and Donovan as follower
self.project_pigs.message_subscribe_users(user_ids=[self.user_projectuser.id, self.user_portal.id, self.user_public.id])
self.issue_1.sudo(self.user_projectmanager.id).message_subscribe_users(user_ids=[self.user_portal.id, self.user_projectuser.id])
@@ -104,11 +92,12 @@ class TestPortalIssue(TestPortalProjectBase):
# Do: Alfred reads project -> ok (follower ok followers)
# Test: followed + assigned issues visible
issues = Issue.sudo(self.user_projectuser.id).search([('project_id', '=', pigs_id)])
self.assertEqual(set(issues.ids), set([self.issue_1.id, self.issue_3.id, self.issue_4.id]),
self.assertEqual(set(issues.ids), test_issue_ids,
'access rights: employee user should not see followed + assigned issues of a follower project')
# Do: Chell reads project -> ok (follower ok follower)
# Test: followed + assigned issues visible
issues = Issue.sudo(self.user_portal.id).search([('project_id', '=', pigs_id)])
self.assertEqual(set(issues.ids), set([self.issue_1.id, self.issue_3.id, self.issue_5.id]),
'access rights: employee user should not see followed + assigned issues of a follower project')
self.assertFalse(set(issues.ids),
'access rights: portal user should not see issues of a follower project')
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_my_home_menu_issue" name="Portal layout : issues menu entry" inherit_id="website_portal.portal_layout" priority="30">
<template id="portal_layout" name="Portal layout: Issues menu entry" inherit_id="website_portal.portal_layout" priority="30">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-att-class="page_name == 'issue' and 'active' or ''">
<a href="/my/issues">Issues</a>
@@ -9,7 +9,7 @@
</xpath>
</template>
<template id="portal_my_home_issue" name="Portal My Home : issue entries" inherit_id="website_portal.portal_my_home" priority="30">
<template id="portal_my_home" name="Portal My Home: Issue entries" inherit_id="website_portal.portal_my_home" priority="30">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 class="page-header">
<a href="/my/issues">Your Issues
@@ -26,39 +26,67 @@
</xpath>
</template>
<template id="portal_my_issues" name="My Issues" priority="30">
<template id="my_issues" name="My Issues">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">Your Issues</h3>
<t t-if="not issues">
<p>There are currently no issues for your account.</p>
</t>
<t t-if="issues">
<table class="table table-hover status_table">
<h3 class="page-header">
Your Issues
<div class="dropdown pull-right mr8">
<button id="sortby" class="btn btn-default" type="button" data-toggle="dropdown">
<span class="fa fa-sort" /> <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-att-href="default_url + '?' + 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-att-href="default_url + '?' + 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 issues">
<div class="alert alert-warning mt8" role="alert">
There are no issues.
</div>
</t>
<table t-if="issues" class="table table-hover status_table">
<thead>
<tr class="active">
<th>Issue</th>
<th>Description</th><th/>
<th>Status</th>
<th class="col-md-8">Description</th>
<th>Stage</th>
</tr>
</thead>
<t t-foreach="issues" t-as="issue">
<tr>
<td>
<a t-attf-href="/my/issues/#{issue.id}?{{ keep_query() }}">Issue <t t-esc="issue.id"/></a>
<a t-attf-href="/my/issues/#{issue.id}?{{ keep_query() }}">Issue <t t-esc="issue.id" /></a>
</td>
<td><span t-field="issue.name"/></td>
<td>
<span class="label label-info" title="Current stage of the issue" t-esc="issue.stage_id.name" />
</td>
<td><span t-field="issue.name"/></td><td/>
<td><span t-field="issue.stage_id.name"/></td>
</tr>
</t>
</table>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</t>
</div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</t>
</template>
<template id="issues_followup" name="Project Issues">
<template id="my_issues_issue" name="My Issue">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
@@ -122,4 +150,29 @@
</div>
</t>
</template>
<template id="my_projects" inherit_id="website_project.my_projects">
<xpath expr="//table/thead/tr/th[last()]" position="after">
<th></th>
</xpath>
<xpath expr="//table/tbody/tr/td[last()]" position="after">
<td>
<a t-if="project.use_issues" t-attf-href="/my/issues?project=#{project.id}">
<t t-esc="project.issue_count" />
<t t-esc="project.label_issues" />
</a>
</td>
</xpath>
</template>
<template id="my_project" inherit_id="website_project.my_project">
<xpath expr="//div[@class='panel-body']/div[@class='row']/div[@class='col-md-6'][last()]/div[@class='pull-right']" position="before">
<a t-attf-href="/my/issues?project=#{project.id}" class="btn btn-default btn-lg pull-right ml8">
<span class="fa fa-bug" />
<span t-esc="project.issue_count" />
<span t-field="project.label_issues" />
</a>
</xpath>
</template>
</odoo>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="issues_followup_sheet" inherit_id="website_project_issue.issues_followup" name="Project Issues Timesheets">
<template id="my_issues_issue" inherit_id="website_project_issue.my_issues_issue" name="Project Issues Timesheets">
<xpath expr="//div[@class='panel-body']" position="inside">
<t t-if="issue.timesheet_ids">
<hr/>
@@ -16,7 +16,7 @@
<tr t-foreach="issue.timesheet_ids" t-as="timesheet">
<td><span t-esc="timesheet.date"/></td>
<td><span t-esc="timesheet.name"/></td>
<td><span t-esc="timesheet.user_id.name"/></td>
<td><span t-esc="timesheet.sudo().user_id.name"/></td>
<td><span t-field="timesheet.unit_amount" t-options='{"widget": "duration", "unit": "hour"}'/></td>
</tr>
</table>
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Timesheet in Website Portal',
'category': 'Website',
'description': """
Display Timesheet on Task in the Website Portal
===============================================
""",
'depends': ['website_project', 'hr_timesheet'],
'data': [
'security/ir.model.access.csv',
'security/account_analytic_line_security.xml',
'views/account_analytic_line_templates.xml',
],
'auto_install': True,
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="portal_task_timesheet_rule" model="ir.rule">
<field name="name">Project/Task Timesheets: frontend access</field>
<field name="model_id" ref="analytic.model_account_analytic_line"/>
<field name="domain_force">['|',
'&amp;',
('task_id.project_id.privacy_visibility', '=', 'portal'),
('task_id.project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('task_id.project_id.privacy_visibility', '=', 'portal'),
('task_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</data>
</openerp>
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_timesheets_portal,access_timesheets_portal,analytic.model_account_analytic_line,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_timesheets_portal access_timesheets_portal analytic.model_account_analytic_line base.group_portal 1 0 0 0
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="my_task" inherit_id="website_project.my_task" name="Project Task Timesheets">
<xpath expr="//div[@class='panel-body']" position="inside">
<div class="container" t-if="task.timesheet_ids">
<hr/>
<table class="table table-hover">
<thead>
<tr class="active">
<th>Date</th>
<th class="col-md-6">Description</th>
<th>Responsible</th>
<th>Duration</th>
</tr>
</thead>
<tr t-foreach="task.timesheet_ids" t-as="timesheet">
<td><t t-esc="timesheet.date"/></td>
<td><t t-esc="timesheet.name"/></td>
<td><t t-esc="timesheet.sudo().user_id.name"/></td>
<td><span t-field="timesheet.unit_amount" t-field-options='{"widget": "duration", "unit": "hour"}'/></td>
</tr>
</table>
</div>
</xpath>
</template>
</odoo>