19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:56 +01:00
parent a2f74aefd8
commit 4a4d12c333
844 changed files with 212348 additions and 270090 deletions

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal

View file

@ -1,16 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from collections import OrderedDict
from operator import itemgetter
from markupsafe import Markup
from odoo import conf, http, _
from odoo.exceptions import AccessError, MissingError
from odoo import http, _
from odoo.exceptions import AccessError, MissingError, UserError
from odoo.fields import Domain
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
from odoo.tools import groupby as groupbyelem
from odoo.osv.expression import OR, AND
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
class ProjectCustomerPortal(CustomerPortal):
@ -19,10 +21,10 @@ class ProjectCustomerPortal(CustomerPortal):
values = super()._prepare_home_portal_values(counters)
if 'project_count' in counters:
values['project_count'] = request.env['project.project'].search_count([]) \
if request.env['project.project'].check_access_rights('read', raise_exception=False) else 0
if request.env['project.project'].has_access('read') else 0
if 'task_count' in counters:
values['task_count'] = request.env['project.task'].search_count([('project_id', '!=', False)]) \
if request.env['project.task'].check_access_rights('read', raise_exception=False) else 0
values['task_count'] = request.env['project.task'].search_count([('project_id', '!=', False)])\
if request.env['project.task'].has_access('read') else 0
return values
# ------------------------------------------------------------
@ -33,7 +35,7 @@ class ProjectCustomerPortal(CustomerPortal):
domain = [('project_id', '=', project.id)]
# pager
url = "/my/projects/%s" % project.id
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url, domain, su=bool(access_token))
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url, domain, su=bool(access_token) and request.env.user.has_group('base.group_public'), project=project)
# adding the access_token to the pager's url args,
# so we are not prompted for loging when switching pages
# if access_token is None, the arg is not present in the URL
@ -45,7 +47,9 @@ class ProjectCustomerPortal(CustomerPortal):
page_name='project',
pager=pager,
project=project,
multiple_projects=False,
task_url=f'projects/{project.id}/task',
preview_object=project,
)
# default value is set to 'project' in _prepare_tasks_values, so we have to set it to 'none' here.
if not groupby:
@ -54,7 +58,7 @@ class ProjectCustomerPortal(CustomerPortal):
return self._get_page_view_values(project, access_token, values, 'my_projects_history', False, **kwargs)
def _prepare_project_domain(self):
return []
return [('is_template', '=', False)]
def _prepare_searchbar_sortings(self):
return {
@ -69,8 +73,8 @@ class ProjectCustomerPortal(CustomerPortal):
domain = self._prepare_project_domain()
searchbar_sortings = self._prepare_searchbar_sortings()
if not sortby or sortby not in searchbar_sortings:
sortby = 'date'
if not sortby:
sortby = 'name'
order = searchbar_sortings[sortby]['order']
if date_begin and date_end:
@ -103,56 +107,41 @@ class ProjectCustomerPortal(CustomerPortal):
})
return request.render("project.portal_my_projects", values)
@http.route(['/my/project/<int:project_id>',
'/my/project/<int:project_id>/page/<int:page>',
'/my/project/<int:project_id>/task/<int:task_id>',
'/my/project/<int:project_id>/project_sharing'], type='http', auth="public")
def portal_project_routes_outdated(self, **kwargs):
""" Redirect the outdated routes to the new routes. """
return request.redirect(request.httprequest.full_path.replace('/my/project/', '/my/projects/'))
@http.route(['/my/task',
'/my/task/page/<int:page>',
'/my/task/<int:task_id>'], type='http', auth='public')
def portal_my_task_routes_outdated(self, **kwargs):
""" Redirect the outdated routes to the new routes. """
return request.redirect(request.httprequest.full_path.replace('/my/task', '/my/tasks'))
@http.route(['/my/projects/<int:project_id>', '/my/projects/<int:project_id>/page/<int:page>'], type='http', auth="public", website=True)
def portal_my_project(self, project_id=None, access_token=None, page=1, date_begin=None, date_end=None, sortby=None, search=None, search_in='content', groupby=None, task_id=None, **kw):
try:
project_sudo = self._document_check_access('project.project', project_id, access_token)
if project_sudo.is_template:
return request.redirect('/my')
except (AccessError, MissingError):
return request.redirect('/my')
if project_sudo.collaborator_count and project_sudo.with_user(request.env.user)._check_project_sharing_access():
values = {'project_id': project_id}
if task_id:
values['task_id'] = task_id
return request.render("project.project_sharing_portal", values)
return request.redirect(f'/my/projects/{project_id}/project_sharing')
project_sudo = project_sudo if access_token else project_sudo.with_user(request.env.user)
if not groupby:
groupby = 'stage_id'
values = self._project_get_page_view_values(project_sudo, access_token, page, date_begin, date_end, sortby, search, search_in, groupby, **kw)
return request.render("project.portal_my_project", values)
def _prepare_project_sharing_session_info(self, project, task=None):
def _get_project_sharing_company(self, project):
return project.company_id or request.env.user.company_id
def _prepare_project_sharing_session_info(self, project):
session_info = request.env['ir.http'].session_info()
user_context = dict(request.env.context) if request.session.uid else {}
mods = conf.server_wide_modules or []
if request.env.lang:
lang = request.env.lang
session_info['user_context']['lang'] = lang
# Update Cache
user_context['lang'] = lang
lang = user_context.get("lang")
translation_hash = request.env['ir.http'].get_web_translations_hash(mods, lang)
cache_hashes = {
"translations": translation_hash,
}
project_company = project.company_id
project_company = self._get_project_sharing_company(project)
session_info.update(
cache_hashes=cache_hashes,
action_name='project.project_sharing_project_task_action',
action_name="project.project_sharing_project_task_action",
project_id=project.id,
project_name=project.name,
user_companies={
'current_company': project_company.id,
'allowed_companies': {
@ -163,21 +152,25 @@ class ProjectCustomerPortal(CustomerPortal):
},
},
# FIXME: See if we prefer to give only the currency that the portal user just need to see the correct information in project sharing
currencies=request.env['ir.http'].get_currencies(),
currencies=request.env['res.currency'].get_all_currencies(),
)
if task:
session_info['open_task_action'] = task.action_project_sharing_open_task()
session_info['user_context'].update({
'allow_milestones': project.allow_milestones,
'allow_task_dependencies': project.allow_task_dependencies,
})
return session_info
@http.route("/my/projects/<int:project_id>/project_sharing", type="http", auth="user", methods=['GET'])
def render_project_backend_view(self, project_id, task_id=None):
@http.route(['/my/projects/<int:project_id>/project_sharing', '/my/projects/<int:project_id>/project_sharing/<path:subpath>'], type='http', auth='user', methods=['GET'])
def render_project_backend_view(self, project_id, subpath=None):
project = request.env['project.project'].sudo().browse(project_id)
if not project.exists() or not project.with_user(request.env.user)._check_project_sharing_access():
if not (
project.exists()
and project.with_user(request.env.user)._check_project_sharing_access()
):
return request.not_found()
task = task_id and request.env['project.task'].browse(int(task_id))
return request.render(
'project.project_sharing_embed',
{'session_info': self._prepare_project_sharing_session_info(project, task)},
'project.project_sharing_portal',
{'session_info': self._prepare_project_sharing_session_info(project)},
)
@http.route('/my/projects/<int:project_id>/task/<int:task_id>', type='http', auth='public', website=True)
@ -200,14 +193,15 @@ class ProjectCustomerPortal(CustomerPortal):
try:
project_sudo = self._document_check_access('project.project', project_id)
task_sudo = request.env['project.task'].search([('project_id', '=', project_id), ('id', '=', task_id)]).sudo()
task_domain = [('id', 'child_of', task_id), ('id', '!=', task_id)]
searchbar_filters = self._get_my_tasks_searchbar_filters([('id', '=', task_sudo.project_id.id)], task_domain)
task_domain = Domain('id', 'child_of', task_id) & Domain('id', '!=', task_id)
searchbar_filters = self._get_my_tasks_searchbar_filters(Domain('id', '=', task_sudo.project_id.id), task_domain)
if not filterby:
filterby = 'all'
domain = searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain']
domain = Domain(searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain'])
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby, url=f'/my/projects/{project_id}/task/{task_id}/subtasks', domain=AND([task_domain, domain]))
values = self._prepare_tasks_values(page, date_begin, date_end, sortby, search, search_in, groupby,
url=f'/my/projects/{project_id}/task/{task_id}/subtasks', domain=task_domain & domain)
values['page_name'] = 'project_subtasks'
# pager
@ -217,6 +211,7 @@ class ProjectCustomerPortal(CustomerPortal):
values.update({
'project': project_sudo,
'show_project': True,
'task': task_sudo,
'grouped_tasks': values['grouped_tasks'](pager['offset']),
'pager': pager,
@ -227,6 +222,38 @@ class ProjectCustomerPortal(CustomerPortal):
except (AccessError, MissingError):
return request.not_found()
@http.route('/my/projects/<int:project_id>/task/<int:task_id>/recurrent_tasks', type='http', auth='user', methods=['GET'], website=True)
def portal_my_project_recurrent_tasks(self, project_id, task_id, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw):
try:
project_sudo = self._document_check_access('project.project', project_id)
task_sudo = request.env['project.task'].search([('project_id', '=', project_id), ('id', '=', task_id)], limit=1).sudo()
task_domain = Domain('id', 'in', task_sudo.recurrence_id.task_ids.ids)
searchbar_filters = self._get_my_tasks_searchbar_filters(Domain('id', '=', project_id), task_domain)
if not filterby:
filterby = 'all'
domain = Domain(searchbar_filters.get(filterby, searchbar_filters.get('all'))['domain'])
values = self._prepare_tasks_values(
page, date_begin, date_end, sortby, search, search_in, groupby,
url=f'/my/projects/{project_id}/task/{task_id}/recurrent_tasks',
domain=task_domain & domain,
)
values['page_name'] = 'project_recurrent_tasks'
pager = portal_pager(**values['pager'])
values.update({
'project': project_sudo,
'task': task_sudo,
'grouped_tasks': values['grouped_tasks'](pager['offset']),
'pager': pager,
'searchbar_filters': dict(sorted(searchbar_filters.items())),
'filterby': filterby,
})
return request.render("project.portal_my_tasks", values)
except (AccessError, MissingError):
return request.not_found()
# ------------------------------------------------------------
# My Task
# ------------------------------------------------------------
@ -245,10 +272,12 @@ class ProjectCustomerPortal(CustomerPortal):
project_accessible = False
values = {
'page_name': page_name,
'priority_labels': dict(task._fields['priority']._description_selection(request.env)),
'task': task,
'user': request.env.user,
'project_accessible': project_accessible,
'task_link_section': [],
'preview_object': task,
}
values = self._get_page_view_values(task, access_token, values, history, False, **kwargs)
@ -268,134 +297,127 @@ class ProjectCustomerPortal(CustomerPortal):
return values
def _task_get_searchbar_sortings(self, milestones_allowed):
def _task_get_searchbar_sortings(self, milestones_allowed, project=False):
values = {
'date': {'label': _('Newest'), 'order': 'create_date desc', 'sequence': 1},
'name': {'label': _('Title'), 'order': 'name', 'sequence': 2},
'project': {'label': _('Project'), 'order': 'project_id, stage_id', 'sequence': 3},
'stage': {'label': _('Stage'), 'order': 'stage_id, project_id', 'sequence': 5},
'status': {'label': _('Status'), 'order': 'kanban_state', 'sequence': 6},
'priority': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 8},
'date_deadline': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 9},
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 11},
'id desc': {'label': _('Newest'), 'order': 'id desc', 'sequence': 10},
'name': {'label': _('Title'), 'order': 'name', 'sequence': 20},
'stage_id, project_id': {'label': _('Stage'), 'order': 'stage_id, project_id', 'sequence': 50},
'state': {'label': _('Status'), 'order': 'state', 'sequence': 60},
'priority desc': {'label': _('Priority'), 'order': 'priority desc', 'sequence': 80},
'date_deadline asc': {'label': _('Deadline'), 'order': 'date_deadline asc', 'sequence': 90},
'date_last_stage_update desc': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc', 'sequence': 110},
}
if not project:
values['project_id, stage_id'] = {'label': _('Project'), 'order': 'project_id, stage_id', 'sequence': 30}
if milestones_allowed:
values['milestone'] = {'label': _('Milestone'), 'order': 'milestone_id', 'sequence': 7}
values['milestone_id'] = {'label': _('Milestone'), 'order': 'milestone_id', 'sequence': 70}
return values
def _task_get_searchbar_groupby(self, milestones_allowed):
def _task_get_searchbar_groupby(self, milestones_allowed, project=False):
values = {
'none': {'input': 'none', 'label': _('None'), 'order': 1},
'project': {'input': 'project', 'label': _('Project'), 'order': 2},
'stage': {'input': 'stage', 'label': _('Stage'), 'order': 4},
'status': {'input': 'status', 'label': _('Status'), 'order': 5},
'priority': {'input': 'priority', 'label': _('Priority'), 'order': 7},
'customer': {'input': 'customer', 'label': _('Customer'), 'order': 10},
'none': {'label': _('None'), 'sequence': 10},
'stage_id': {'label': _('Stage'), 'sequence': 20},
'state': {'label': _('Status'), 'sequence': 40},
'priority': {'label': _('Priority'), 'sequence': 60},
'partner_id': {'label': _('Customer'), 'sequence': 70},
}
if not project:
values['project_id'] = {'label': _('Project'), 'sequence': 30}
if milestones_allowed:
values['milestone'] = {'input': 'milestone', 'label': _('Milestone'), 'order': 6}
return dict(sorted(values.items(), key=lambda item: item[1]["order"]))
values['milestone_id'] = {'label': _('Milestone'), 'sequence': 50}
return values
def _task_get_groupby_mapping(self):
return {
'project': 'project_id',
'stage': 'stage_id',
'customer': 'partner_id',
'milestone': 'milestone_id',
'priority': 'priority',
'status': 'kanban_state',
}
def _task_get_order(self, order, groupby):
groupby_mapping = self._task_get_groupby_mapping()
field_name = groupby_mapping.get(groupby, '')
if not field_name:
return order
return '%s, %s' % (field_name, order)
def _task_get_searchbar_inputs(self, milestones_allowed):
def _task_get_searchbar_inputs(self, milestones_allowed, project=False):
values = {
'all': {'input': 'all', 'label': _('Search in All'), 'order': 1},
'content': {'input': 'content', 'label': Markup(_('Search <span class="nolabel"> (in Content)</span>')), 'order': 1},
'ref': {'input': 'ref', 'label': _('Search in Ref'), 'order': 1},
'project': {'input': 'project', 'label': _('Search in Project'), 'order': 2},
'users': {'input': 'users', 'label': _('Search in Assignees'), 'order': 3},
'stage': {'input': 'stage', 'label': _('Search in Stages'), 'order': 4},
'status': {'input': 'status', 'label': _('Search in Status'), 'order': 5},
'priority': {'input': 'priority', 'label': _('Search in Priority'), 'order': 7},
'message': {'input': 'message', 'label': _('Search in Messages'), 'order': 11},
'name': {'input': 'name', 'label': _(
'Search%(left)s Tasks%(right)s',
left=Markup('<span class="nolabel">'),
right=Markup('</span>'),
), 'sequence': 10},
'users': {'input': 'user_ids', 'label': _('Search in Assignees'), 'sequence': 20},
'stage_id': {'input': 'stage_id', 'label': _('Search in Stages'), 'sequence': 30},
'status': {'input': 'status', 'label': _('Search in Status'), 'sequence': 40},
'priority': {'input': 'priority', 'label': _('Search in Priority'), 'sequence': 60},
'partner_id': {'input': 'partner_id', 'label': _('Search in Customer'), 'sequence': 80},
}
if not project:
values['project_id'] = {'input': 'project_id', 'label': _('Search in Project'), 'sequence': 50}
if milestones_allowed:
values['milestone'] = {'input': 'milestone', 'label': _('Search in Milestone'), 'order': 6}
values['milestone_id'] = {'input': 'milestone_id', 'label': _('Search in Milestone'), 'sequence': 70}
return dict(sorted(values.items(), key=lambda item: item[1]["order"]))
return values
def _task_get_search_domain(self, search_in, search):
search_domain = []
if search_in in ('content', 'all'):
search_domain.append([('name', 'ilike', search)])
search_domain.append([('description', 'ilike', search)])
if search_in in ('customer', 'all'):
search_domain.append([('partner_id', 'ilike', search)])
if search_in in ('message', 'all'):
search_domain.append([('message_ids.body', 'ilike', search)])
if search_in in ('stage', 'all'):
search_domain.append([('stage_id', 'ilike', search)])
if search_in in ('project', 'all'):
search_domain.append([('project_id', 'ilike', search)])
if search_in in ('ref', 'all'):
search_domain.append([('id', 'ilike', search)])
if search_in in ('milestone', 'all'):
search_domain.append([('milestone_id', 'ilike', search)])
if search_in in ('users', 'all'):
def _task_get_search_domain(self, search_in, search, milestones_allowed, project):
if not search_in or search_in == 'name':
return ['|', ('name', 'ilike', search), ('id', 'ilike', search)]
elif search_in == 'users':
user_ids = request.env['res.users'].sudo().search([('name', 'ilike', search)])
search_domain.append([('user_ids', 'in', user_ids.ids)])
if search_in in ('priority', 'all'):
search_domain.append([('priority', 'ilike', search == 'normal' and '0' or '1')])
if search_in in ('status', 'all'):
search_domain.append([
('kanban_state', 'ilike', 'normal' if search == 'In Progress' else 'done' if search == 'Ready' else 'blocked' if search == 'Blocked' else search)
return [('user_ids', 'in', user_ids.ids)]
elif search_in == 'priority':
priority_selections = request.env['ir.model.fields.selection'].sudo().search([
('field_id.model', '=', 'project.task'),
('field_id.name', '=', 'priority'),
('name', 'ilike', search),
])
return OR(search_domain)
if priority_selections:
return [('priority', 'in', priority_selections.mapped('value'))]
return Domain.FALSE
elif search_in == 'status':
state_selections = request.env['ir.model.fields.selection'].sudo().search([
('field_id.model', '=', 'project.task'),
('field_id.name', '=', 'state'),
('name', 'ilike', search),
])
if state_selections:
return [('state', 'in', state_selections.mapped('value'))]
return Domain.FALSE
elif search_in in self._task_get_searchbar_inputs(milestones_allowed, project):
return [(search_in, 'ilike', search)]
else:
return ['|', ('name', 'ilike', search), ('id', 'ilike', search)]
def _prepare_tasks_values(self, page, date_begin, date_end, sortby, search, search_in, groupby, url="/my/tasks", domain=None, su=False):
def _prepare_tasks_values(self, page, date_begin, date_end, sortby, search, search_in, groupby, url="/my/tasks", domain=None, su=False, project=False):
values = self._prepare_portal_layout_values()
Task = request.env['project.task']
milestone_domain = AND([domain, [('allow_milestones', '=', 'True')]])
milestones_allowed = Task.sudo().search_count(milestone_domain, limit=1) == 1
searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings(milestones_allowed).items(),
key=lambda item: item[1]["sequence"]))
searchbar_inputs = self._task_get_searchbar_inputs(milestones_allowed)
searchbar_groupby = self._task_get_searchbar_groupby(milestones_allowed)
if not domain:
domain = []
if not su and Task.check_access_rights('read'):
domain = AND([domain, request.env['ir.rule']._compute_domain(Task._name, 'read')])
domain = Domain.AND([domain or [], [('has_template_ancestor', '=', False)]])
if not su and Task.has_access('read'):
domain &= Domain(request.env['ir.rule']._compute_domain(Task._name, 'read'))
Task_sudo = Task.sudo()
milestone_domain = domain & Domain('allow_milestones', '=', True) & Domain('milestone_id', '!=', False)
milestones_allowed = Task_sudo.search_count(milestone_domain, limit=1) == 1
searchbar_sortings = dict(sorted(self._task_get_searchbar_sortings(milestones_allowed, project).items(),
key=lambda item: item[1]["sequence"]))
searchbar_inputs = dict(sorted(self._task_get_searchbar_inputs(milestones_allowed, project).items(), key=lambda item: item[1]['sequence']))
searchbar_groupby = dict(sorted(self._task_get_searchbar_groupby(milestones_allowed, project).items(), key=lambda item: item[1]['sequence']))
# default sort by value
if not sortby or sortby not in searchbar_sortings or (sortby == 'milestone' and not milestones_allowed):
sortby = 'date'
order = searchbar_sortings[sortby]['order']
if not sortby or (sortby == 'milestone_id' and not milestones_allowed):
sortby = next(iter(searchbar_sortings))
# default group by value
if not groupby or (groupby == 'milestone' and not milestones_allowed):
groupby = 'project'
if not groupby or (groupby == 'milestone_id' and not milestones_allowed):
groupby = 'project_id'
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
domain &= Domain('create_date', '>', date_begin) & Domain('create_date', '<=', date_end)
# search reset if needed
if not milestones_allowed and search_in == 'milestone':
if not milestones_allowed and search_in == 'milestone_id':
search_in = 'all'
# search
if search and search_in:
domain += self._task_get_search_domain(search_in, search)
domain &= Domain(self._task_get_search_domain(search_in, search, milestones_allowed, project))
# content according to pager and archive selected
order = self._task_get_order(order, groupby)
if groupby == 'none':
group_field = None
elif groupby == 'priority':
group_field = 'priority desc'
else:
group_field = groupby
order = '%s, %s' % (group_field, sortby) if group_field else sortby
def get_grouped_tasks(pager_offset):
tasks = Task_sudo.search(domain, order=order, limit=self._items_per_page, offset=pager_offset)
@ -404,11 +426,9 @@ class ProjectCustomerPortal(CustomerPortal):
tasks_project_allow_milestone = tasks.filtered(lambda t: t.allow_milestones)
tasks_no_milestone = tasks - tasks_project_allow_milestone
groupby_mapping = self._task_get_groupby_mapping()
group = groupby_mapping.get(groupby)
if group:
if group == 'milestone_id':
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks_project_allow_milestone, itemgetter(group))]
if groupby != 'none':
if groupby == 'milestone_id':
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks_project_allow_milestone, itemgetter(groupby))]
if not grouped_tasks:
if tasks_no_milestone:
@ -420,16 +440,16 @@ class ProjectCustomerPortal(CustomerPortal):
grouped_tasks[len(grouped_tasks) - 1] |= tasks_no_milestone
else:
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks, itemgetter(group))]
grouped_tasks = [Task_sudo.concat(*g) for k, g in groupbyelem(tasks, itemgetter(groupby))]
else:
grouped_tasks = [tasks] if tasks else []
task_states = dict(Task_sudo._fields['kanban_state']._description_selection(request.env))
if sortby == 'status':
task_states = dict(Task_sudo._fields['state']._description_selection(request.env))
if sortby == 'state':
if groupby == 'none' and grouped_tasks:
grouped_tasks[0] = grouped_tasks[0].sorted(lambda tasks: task_states.get(tasks.kanban_state))
grouped_tasks[0] = grouped_tasks[0].sorted(lambda tasks: task_states.get(tasks.state))
else:
grouped_tasks.sort(key=lambda tasks: task_states.get(tasks[0].kanban_state))
grouped_tasks.sort(key=lambda tasks: task_states.get(tasks[0].state))
return grouped_tasks
values.update({
@ -437,6 +457,8 @@ class ProjectCustomerPortal(CustomerPortal):
'date_end': date_end,
'grouped_tasks': get_grouped_tasks,
'allow_milestone': milestones_allowed,
'multiple_projects': True,
'priority_labels': dict(Task_sudo._fields['priority']._description_selection(request.env)),
'page_name': 'task',
'default_url': url,
'task_url': 'tasks',
@ -459,11 +481,11 @@ class ProjectCustomerPortal(CustomerPortal):
def _get_my_tasks_searchbar_filters(self, project_domain=None, task_domain=None):
searchbar_filters = {
'all': {'label': _('All'), 'domain': [('project_id', '!=', False)]},
'all': {'label': _('All'), 'domain': [('project_id', '!=', False), ('is_template', '=', False)]},
}
# extends filterby criteria with project the customer has access to
projects = request.env['project.project'].search(project_domain or [])
projects = request.env['project.project'].search(project_domain or [], order="id")
for project in projects:
searchbar_filters.update({
str(project.id): {'label': project.name, 'domain': [('project_id', '=', project.id)]}
@ -471,19 +493,18 @@ class ProjectCustomerPortal(CustomerPortal):
# extends filterby criteria with project (criteria name is the project id)
# Note: portal users can't view projects they don't follow
project_groups = request.env['project.task'].read_group(AND([[('project_id', 'not in', projects.ids)], task_domain or []]),
['project_id'], ['project_id'])
for group in project_groups:
proj_id = group['project_id'][0] if group['project_id'] else False
proj_name = group['project_id'][1] if group['project_id'] else _('Others')
project_groups = request.env['project.task']._read_group(
Domain.AND([[('project_id', 'not in', projects.ids), ('is_template', '=', False), ('project_id', '!=', False)], task_domain or []]),
['project_id'])
for [project] in project_groups:
searchbar_filters.update({
str(proj_id): {'label': proj_name, 'domain': [('project_id', '=', proj_id)]}
str(project.id): {'label': project.sudo().display_name, 'domain': [('project_id', '=', project.id)]}
})
return searchbar_filters
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='content', groupby=None, **kw):
searchbar_filters = self._get_my_tasks_searchbar_filters()
def portal_my_tasks(self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, search=None, search_in='name', groupby=None, **kw):
searchbar_filters = self._get_my_tasks_searchbar_filters([('is_template', '=', False)])
if not filterby:
filterby = 'all'
@ -496,10 +517,12 @@ class ProjectCustomerPortal(CustomerPortal):
pager_vals['url_args'].update(filterby=filterby)
pager = portal_pager(**pager_vals)
grouped_tasks = values['grouped_tasks'](pager['offset'])
values.update({
'grouped_tasks': values['grouped_tasks'](pager['offset']),
'grouped_tasks': grouped_tasks,
'show_project': True,
'pager': pager,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'searchbar_filters': searchbar_filters,
'filterby': filterby,
})
return request.render("project.portal_my_tasks", values)
@ -528,3 +551,42 @@ class ProjectCustomerPortal(CustomerPortal):
request.session['my_tasks_history'] = task_sudo.ids
values = self._task_get_page_view_values(task_sudo, access_token, **kw)
return request.render("project.portal_my_task", values)
@http.route('/project_sharing/attachment/add_image', type='http', auth='user', methods=['POST'], website=True)
def add_image(self, name, data, res_id, access_token=None, **kwargs):
try:
task_sudo = self._document_check_access('project.task', int(res_id), access_token=access_token)
if not task_sudo.with_user(request.env.uid).project_id._check_project_sharing_access():
return request.not_found()
except (AccessError, MissingError):
raise UserError(_("The document does not exist or you do not have the rights to access it."))
IrAttachment = request.env['ir.attachment']
# Avoid using sudo when not necessary: internal users can create attachments,
# as opposed to public and portal users.
if not request.env.user._is_internal():
IrAttachment = IrAttachment.sudo()
values = IrAttachment._check_contents({
'name': name,
'datas': data,
'res_model': 'project.task',
'res_id': res_id,
'access_token': IrAttachment._generate_access_token(),
})
valid_image_mime_types = ['image/jpeg', 'image/png', 'image/bmp', 'image/tiff']
if values.get('mimetype', False) not in valid_image_mime_types:
return request.make_response(
data=json.dumps({'error': _('Only jpeg, png, bmp and tiff images are allowed as attachments.')}),
headers=[('Content-Type', 'application/json')],
status=400
)
attachment = IrAttachment.create(values)
return request.make_response(
data=json.dumps(attachment.read(['id', 'name', 'mimetype', 'file_size', 'access_token'])[0]),
headers=[('Content-Type', 'application/json')]
)

View file

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import Forbidden
from odoo.http import request, route
from odoo.http import request
from odoo.addons.portal.controllers.mail import PortalChatter
from odoo.addons.portal.controllers.portal_thread import PortalChatter
from .portal import ProjectCustomerPortal
@ -25,77 +24,7 @@ class ProjectSharingChatter(PortalChatter):
can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access()
task = None
if can_access:
task = request.env['project.task'].sudo().search([('id', '=', res_id), ('project_id', '=', project_sudo.id)])
task = request.env['project.task'].sudo().with_context(active_test=False).search([('id', '=', res_id), ('project_id', '=', project_sudo.id)])
if not can_access or not task:
raise Forbidden()
return task[task._mail_post_token_field]
# ============================================================ #
# Note concerning the methods portal_chatter_(init/post/fetch)
# ============================================================ #
#
# When the project is shared to a portal user with the edit rights,
# he has the read/write access to the related tasks. So it could be
# possible to call directly the message_post method on a task.
#
# This change is considered as safe, as we only willingly expose
# records, for some assumed fields only, and this feature is
# optional and opt-in. (like the public employee model for example).
# It doesn't allow portal users to access other models, like
# a timesheet or an invoice.
#
# It could seem odd to use those routes, and converting the project
# access token into the task access token, as the user has actually
# access to the records.
#
# However, it has been decided that it was the less hacky way to
# achieve this, as:
#
# - We're reusing the existing routes, that convert all the data
# into valid arguments for the methods we use (message_post, ...).
# That way, we don't have to reinvent the wheel, duplicating code
# from mail/portal that surely will lead too desynchronization
# and inconsistencies over the time.
#
# - We don't define new routes, to do the exact same things than portal,
# considering that the portal user can use message_post for example
# because he has access to the record.
# Let's suppose that we remove this in a future development, those
# new routes won't be valid anymore.
#
# - We could have reused the mail widgets, as we already reuse the
# form/list/kanban views, etc. However, we only want to display
# the messages and allow to post. We don't need the next activities
# the followers system, etc. This required to override most of the
# mail.thread basic methods, without being sure that this would
# work with other installed applications or customizations
@route()
def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
project_sharing_id = kwargs.get('project_sharing_id')
if project_sharing_id:
# if there is a token in `kwargs` then it should be the access_token of the project shared
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kwargs.get('token'))
if token:
del kwargs['project_sharing_id']
kwargs['token'] = token
return super().portal_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs)
@route()
def portal_chatter_post(self, res_model, res_id, message, attachment_ids=None, attachment_tokens=None, **kw):
project_sharing_id = kw.get('project_sharing_id')
if project_sharing_id:
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kw.get('token'))
if token:
del kw['project_sharing_id']
kw['token'] = token
return super().portal_chatter_post(res_model, res_id, message, attachment_ids=attachment_ids, attachment_tokens=attachment_tokens, **kw)
@route()
def portal_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw):
project_sharing_id = kw.get('project_sharing_id')
if project_sharing_id:
token = self._check_project_access_and_get_token(project_sharing_id, res_model, res_id, kw.get('token'))
if token is not None:
kw['token'] = token # Update token (either string which contains token value or False)
return super().portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw)