diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index 51eb66b03ae..8295c7bd025 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -27,6 +27,7 @@ up a management by affair. 'views/hr_timesheet_views.xml', 'views/hr_timesheet_config_settings_views.xml', 'views/project_views.xml', + 'views/project_portal_templates.xml', 'report/hr_timesheet_report_view.xml', 'report/report_timesheet_templates.xml', 'views/hr_views.xml', diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 23d20ea4fd4..2a8b9743d2f 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -51,6 +51,21 @@ + + Project/Task Timesheets: frontend access + + [ + '|', + '&', + ('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]), + ] + + + \ No newline at end of file diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index e9f8ce09b30..661b02a5a3c 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -1,8 +1,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1 +access_account_analytic_line_portal,access_timesheets_portal,analytic.model_account_analytic_line,base.group_portal,1,0,0,0 access_account_analytic_user,analytic.account.analytic.timesheet.user,analytic.model_account_analytic_account,hr_timesheet.group_hr_timesheet_user,1,1,0,0 access_product_product_user,product.product.timesheet.user,product.model_product_product,hr_timesheet.group_hr_timesheet_user,1,1,1,0 access_product_template_timesheet_user,product.template.timesheet.user,product.model_product_template,hr_timesheet.group_hr_timesheet_user,1,1,1,0 access_product_uom_hr_timesheet,product.uom.timesheet.user,product.model_product_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_project,project.project.timesheet.user,model_project_project,hr_timesheet.group_hr_timesheet_user,1,0,0,0 -access_project_task,project.task.timesheet.user,model_project_task,hr_timesheet.group_hr_timesheet_user,1,1,0,0 \ No newline at end of file +access_project_task,project.task.timesheet.user,model_project_task,hr_timesheet.group_hr_timesheet_user,1,1,0,0 diff --git a/addons/website_project_timesheet/views/account_analytic_line_templates.xml b/addons/hr_timesheet/views/project_portal_templates.xml similarity index 90% rename from addons/website_project_timesheet/views/account_analytic_line_templates.xml rename to addons/hr_timesheet/views/project_portal_templates.xml index f48b0739794..cb0ed74e25c 100644 --- a/addons/website_project_timesheet/views/account_analytic_line_templates.xml +++ b/addons/hr_timesheet/views/project_portal_templates.xml @@ -1,7 +1,7 @@ - + + + + diff --git a/addons/project/__init__.py b/addons/project/__init__.py index 0c023b8d77a..7039d459cb8 100644 --- a/addons/project/__init__.py +++ b/addons/project/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import controllers from . import models from . import report from . import wizard \ No newline at end of file diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index a279f96deb4..a4f95b3c203 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -13,6 +13,7 @@ 'product', 'analytic', 'mail', + 'portal', 'resource', 'web', 'web_planner', @@ -28,6 +29,7 @@ 'views/res_partner_views.xml', 'views/project_config_settings_views.xml', 'views/project_templates.xml', + 'views/project_portal_templates.xml', 'data/web_planner_data.xml', 'data/project_mail_template_data.xml', 'wizard/project_task_merge_wizard_views.xml', diff --git a/addons/website_project/controllers/__init__.py b/addons/project/controllers/__init__.py similarity index 82% rename from addons/website_project/controllers/__init__.py rename to addons/project/controllers/__init__.py index 5d4b25db9c0..903b755e71e 100644 --- a/addons/website_project/controllers/__init__.py +++ b/addons/project/controllers/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import main +from . import portal diff --git a/addons/website_project/controllers/main.py b/addons/project/controllers/portal.py similarity index 84% rename from addons/website_project/controllers/main.py rename to addons/project/controllers/portal.py index 4e29fb1ccf4..76079d9bdff 100644 --- a/addons/website_project/controllers/main.py +++ b/addons/project/controllers/portal.py @@ -5,15 +5,16 @@ from collections import OrderedDict from odoo import http, _ from odoo.http import request -from odoo.addons.portal.controllers.portal import get_records_pager, CustomerPortal +from odoo.addons.portal.controllers.portal import get_records_pager, CustomerPortal, pager as portal_pager + from odoo.osv.expression import OR from odoo.tools import pycompat -class WebsiteAccount(CustomerPortal): +class CustomerPortal(CustomerPortal): def _prepare_portal_layout_values(self): - values = super(WebsiteAccount, self)._prepare_portal_layout_values() + values = super(CustomerPortal, self)._prepare_portal_layout_values() values.update({ 'project_count': request.env['project.project'].search_count([('privacy_visibility', '=', 'portal')]), 'task_count': request.env['project.task'].search_count([('project_id.privacy_visibility', '=', 'portal')]) @@ -21,7 +22,7 @@ class WebsiteAccount(CustomerPortal): return values @http.route(['/my/projects', '/my/projects/page/'], type='http', auth="user", website=True) - def my_projects(self, page=1, date_begin=None, date_end=None, sortby=None, **kw): + def portal_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'] domain = [('privacy_visibility', '=', 'portal')] @@ -41,7 +42,7 @@ class WebsiteAccount(CustomerPortal): # projects count project_count = Project.search_count(domain) # pager - pager = request.website.pager( + pager = portal_pager( url="/my/projects", url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby}, total=project_count, @@ -64,17 +65,18 @@ class WebsiteAccount(CustomerPortal): 'searchbar_sortings': searchbar_sortings, 'sortby': sortby }) - return request.render("website_project.my_projects", values) + return request.render("project.portal_my_projects", values) - @http.route(['/my/project/'], type='http', auth="user", website=True) - def my_project(self, project=None, **kw): - vals = {'project': project, } + @http.route(['/my/project/'], type='http', auth="user", website=True) + def portal_my_project(self, project_id=None, **kw): + project = request.env['project.project'].browse(project_id) + vals = {'project': project} history = request.session.get('my_projects_history', []) vals.update(get_records_pager(history, project)) - return request.render("website_project.my_project", vals) + return request.render("project.portal_my_project", vals) @http.route(['/my/tasks', '/my/tasks/page/'], type='http', auth="user", website=True) - def my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', **kw): + def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', **kw): values = self._prepare_portal_layout_values() domain = [('project_id.privacy_visibility', '=', 'portal')] @@ -131,7 +133,7 @@ class WebsiteAccount(CustomerPortal): # task count task_count = request.env['project.task'].search_count(domain) # pager - pager = request.website.pager( + pager = portal_pager( url="/my/tasks", url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby}, total=task_count, @@ -158,11 +160,15 @@ class WebsiteAccount(CustomerPortal): 'searchbar_filters': OrderedDict(sorted(pycompat.items(searchbar_filters))), 'filterby': filterby, }) - return request.render("website_project.my_tasks", values) + return request.render("project.portal_my_tasks", values) - @http.route(['/my/task/'], type='http', auth="user", website=True) - def my_task(self, task=None, **kw): - vals = {'task': task, 'user': request.env.user} + @http.route(['/my/task/'], type='http', auth="user", website=True) + def portal_my_task(self, task_id=None, **kw): + task = request.env['project.task'].browse(task_id) + vals = { + 'task': task, + 'user': request.env.user + } history = request.session.get('my_tasks_history', []) vals.update(get_records_pager(history, task)) - return request.render("website_project.my_task", vals) + return request.render("project.portal_my_task", vals) diff --git a/addons/project/data/project_demo.xml b/addons/project/data/project_demo.xml index ba068404bdf..f5e53c769df 100644 --- a/addons/project/data/project_demo.xml +++ b/addons/project/data/project_demo.xml @@ -67,6 +67,8 @@ 3 + + portal @@ -425,5 +427,8 @@ Send it ASAP, its urgent. + + diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 49014a07481..25c8e52a6dd 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -4,7 +4,7 @@ from lxml import etree from odoo import api, fields, models, tools, SUPERUSER_ID, _ -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import UserError, AccessError from odoo.tools.safe_eval import safe_eval @@ -46,7 +46,7 @@ class ProjectTaskType(models.Model): class Project(models.Model): _name = "project.project" _description = "Project" - _inherit = ['mail.alias.mixin', 'mail.thread'] + _inherit = ['mail.alias.mixin', 'mail.thread', 'portal.mixin'] _inherits = {'account.analytic.account': "analytic_account_id"} _order = "sequence, name, id" _period_number = 5 @@ -225,6 +225,11 @@ class Project(models.Model): ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.') ] + def _compute_portal_url(self): + super(Project, self)._compute_portal_url() + for project in self: + project.portal_url = '/my/project/%s' % project.id + @api.multi def map_tasks(self, new_project_id): """ copy and map tasks from old to new project """ @@ -271,6 +276,30 @@ class Project(models.Model): project.message_subscribe(project.partner_id.ids) return res + @api.multi + def get_access_action(self, access_uid=None): + """ Instead of the classic form view, redirect to website for portal users + that can read the project. """ + self.ensure_one() + user, record = self.env.user, self + if access_uid: + user = self.env['res.users'].sudo().browse(access_uid) + record = self.sudo(user) + + if user.share: + try: + record.check_access_rule('read') + except AccessError: + pass + else: + return { + 'type': 'ir.actions.act_url', + 'url': '/my/project/%s' % self.id, + 'target': 'self', + 'res_id': self.id, + } + return super(Project, self).get_access_action(access_uid) + @api.multi def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, force=True): """ Subscribe to all existing active tasks when subscribing to a project """ @@ -290,6 +319,28 @@ class Project(models.Model): self.mapped('tasks').message_unsubscribe(partner_ids=partner_ids, channel_ids=channel_ids) return super(Project, self).message_unsubscribe(partner_ids=partner_ids, channel_ids=channel_ids) + @api.multi + def _notification_recipients(self, message, groups): + groups = super(Project, self)._notification_recipients(message, groups) + + for group_name, group_method, group_data in groups: + group_data['has_button_access'] = True + + return groups + + @api.multi + def toggle_favorite(self): + favorite_projects = not_fav_projects = self.env['project.project'].sudo() + for project in self: + if self.env.user in project.favorite_user_ids: + favorite_projects |= project + else: + not_fav_projects |= project + + # Project User has no write access for project. + not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]}) + favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]}) + @api.multi def close_dialog(self): return {'type': 'ir.actions.act_window_close'} @@ -299,7 +350,7 @@ class Task(models.Model): _name = "project.task" _description = "Task" _date_name = "date_start" - _inherit = ['mail.thread', 'mail.activity.mixin'] + _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _mail_post_access = 'read' _order = "priority desc, sequence, date_start, name, id" @@ -383,7 +434,8 @@ class Task(models.Model): default=lambda self: self.env['res.company']._company_default_get()) color = fields.Integer(string='Color Index') user_email = fields.Char(related='user_id.email', string='User Email', readonly=True) - attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=lambda self: [('res_model', '=', self._name)], auto_join=True, string='Attachments') + attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments", + help="Attachment that don't come from message.") # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image') legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True) @@ -402,6 +454,12 @@ class Task(models.Model): working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg") working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg") + def _compute_attachment_ids(self): + for task in self: + attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids + message_attachment_ids = self.mapped('message_ids.attachment_ids').ids # from mail_thread + task.attachment_ids = list(set(attachment_ids) - set(message_attachment_ids)) + @api.multi @api.depends('create_date', 'date_end', 'date_assign') def _compute_elapsed(self): @@ -436,6 +494,11 @@ class Task(models.Model): else: task.kanban_state_label = task.legend_done + def _compute_portal_url(self): + super(Task, self)._compute_portal_url() + for task in self: + task.portal_url = '/my/task/%s' % task.id + @api.onchange('partner_id') def _onchange_partner_id(self): self.email_from = self.partner_id.email @@ -601,6 +664,30 @@ class Task(models.Model): return {'date_end': fields.Datetime.now()} return {'date_end': False} + @api.multi + def get_access_action(self, access_uid=None): + """ Instead of the classic form view, redirect to website for portal users + that can read the task. """ + self.ensure_one() + user, record = self.env.user, self + if access_uid: + user = self.env['res.users'].sudo().browse(access_uid) + record = self.sudo(user) + + if user.share: + try: + record.check_access_rule('read') + except AccessError: + pass + else: + return { + 'type': 'ir.actions.act_url', + 'url': '/my/task/%s' % self.id, + 'target': 'self', + 'res_id': self.id, + } + return super(Task, self).get_access_action(access_uid) + # --------------------------------------------------- # Mail gateway # --------------------------------------------------- @@ -647,7 +734,11 @@ class Task(models.Model): 'actions': project_actions, }) - return [new_group] + groups + groups = [new_group] + groups + for group_name, group_method, group_data in groups: + group_data['has_button_access'] = True + + return groups @api.model def message_get_reply_to(self, res_ids, default=None): diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 38cfb7fb212..327c32aeb11 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -3,18 +3,23 @@ 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 +access_project_task_type_portal,task_type_portal,project.model_project_task_type,base.group_portal,1,0,0,0 access_project_task,project.task,model_project_task,project.group_project_user,1,1,1,1 access_report_project_task_user,report.project.task.user,model_report_project_task_user,project.group_project_manager,1,1,1,1 access_partner_task user,base.res.partner user,base.model_res_partner,project.group_project_user,1,0,0,0 access_task_on_partner,project.task on partners,model_project_task,base.group_user,1,0,0,0 -access_project_on_partner,project.project on partners,model_project_project,base.group_user,1,0,0,0 +access_task_portal,task_portal,project.model_project_task,base.group_portal,1,0,0,0 +access_project_user,project.project on partners,model_project_project,base.group_user,1,0,0,0 +access_project_portal,project_portal,project.model_project_project,base.group_portal,1,0,0,0 access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 access_resource_calendar,project.resource_calendar user,resource.model_resource_calendar,project.group_project_user,1,0,0,0 access_resource_calendar_attendance,project.resource_calendar_attendance user,resource.model_resource_calendar_attendance,project.group_project_user,1,0,0,0 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 diff --git a/addons/project/security/project_security.xml b/addons/project/security/project_security.xml index 680d459bd82..5651bc4b2d5 100644 --- a/addons/project/security/project_security.xml +++ b/addons/project/security/project_security.xml @@ -96,5 +96,32 @@ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + Project: portal users: portal and following + + [ + '&', + ('privacy_visibility', '=', 'portal'), + ('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), + ] + + + + + Project/Task: portal users: (portal and following project) or (portal and following task) + + [ + '|', + '&', + ('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]), + ] + + + diff --git a/addons/project/tests/__init__.py b/addons/project/tests/__init__.py index 8e85c184df2..a5fce7273f7 100644 --- a/addons/project/tests/__init__.py +++ b/addons/project/tests/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import test_project_base, test_project_flow, test_access_rights, test_project_ui +from . import test_project_base, test_project_flow, test_access_rights, test_project_ui, test_portal diff --git a/addons/website_project/tests/test_access_rights.py b/addons/project/tests/test_portal.py similarity index 100% rename from addons/website_project/tests/test_access_rights.py rename to addons/project/tests/test_portal.py diff --git a/addons/website_project/views/project_templates.xml b/addons/project/views/project_portal_templates.xml similarity index 96% rename from addons/website_project/views/project_templates.xml rename to addons/project/views/project_portal_templates.xml index c9e4b047d53..7b5d96db751 100644 --- a/addons/website_project/views/project_templates.xml +++ b/addons/project/views/project_portal_templates.xml @@ -32,7 +32,7 @@ -