[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:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
'&',
|
||||
('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">['|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('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']),
|
||||
'&',
|
||||
('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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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">['|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('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">[
|
||||
'|',
|
||||
'&',
|
||||
('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>
|
||||
|
||||
<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']),
|
||||
'&',
|
||||
('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">['|',
|
||||
'&',
|
||||
('task_id.project_id.privacy_visibility', '=', 'portal'),
|
||||
('task_id.project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('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
|
||||
|
@@ -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>
|
||||
Reference in New Issue
Block a user