summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorqho <qho@odoo.com>2020-03-10 07:50:27 +0000
committerYannick Tivisse <yti@odoo.com>2020-06-29 11:56:36 +0000
commit1424a3839a6c304599bdeefcd74f29279ae75420 (patch)
treef78d9388f3dfd55ffdc8b24ac6aaa4304571fd94
parent78e2d2e2fb49814c2f51dd97d2ddba6ba533d2e1 (diff)
[IMP] hr_timesheet: Improve the usability of timesheet portal
1. Generic timesheets improvements - Portal /my/timesheets - Add a group bys task, date, employee - Add search in project, name, employee, task * The task column should not be visible if none of the timesheets have the Task field set - If a group by task, date, employee or project is applied, the corresponding column should not be visible 2. Timesheets from sub-tasks on parent tasks - Portal /my/tasks - Display the 'Total Hours' spent on a task at the end of the list of timesheets - Below the list of timesheets for the task, display another table detailing the respective timesheets (and total hours) of each sub-task linked to this task (= 1 table/list per sub-task) * Title should be: 'Timesheets for sub-task: xyz' * only visible if there are sub-tasks linked to this task * only visible if total hours > 0 3. Timesheets on Invoices - Portal /my/invoices - Add Project + task fields * Task field only visible if set on one of the timesheet entries * Project field only visible if there are at least 2 different projects in these timesheet entries 4. Correct visibility for the portal users - Portal users is allowed to see timesheets if the users are added in allowed portal users list in the project - When a portal user is added as the message follower of the project, the user is added to allowed portal users as well - When a portal user is removed from allowed portal users, the user is also removed from message followers closes odoo/odoo#47248 Taskid: 2201908 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
-rw-r--r--addons/hr_timesheet/controllers/portal.py60
-rw-r--r--addons/hr_timesheet/controllers/project.py11
-rw-r--r--addons/hr_timesheet/models/hr_timesheet.py12
-rw-r--r--addons/hr_timesheet/views/hr_timesheet_portal_templates.xml41
-rw-r--r--addons/hr_timesheet/views/project_portal_templates.xml47
-rw-r--r--addons/mail/static/src/js/basic_view.js2
-rw-r--r--addons/mail/static/src/models/thread/thread.js6
-rw-r--r--addons/mail/static/src/widgets/form_renderer/form_renderer.js4
-rw-r--r--addons/project/models/project.py20
-rw-r--r--addons/project/views/project_views.xml4
-rw-r--r--addons/sale_project/models/sale_order.py1
-rw-r--r--addons/sale_timesheet/views/sale_timesheet_portal_templates.xml23
12 files changed, 195 insertions, 36 deletions
diff --git a/addons/hr_timesheet/controllers/portal.py b/addons/hr_timesheet/controllers/portal.py
index 95646b94dcd..aceb00e3232 100644
--- a/addons/hr_timesheet/controllers/portal.py
+++ b/addons/hr_timesheet/controllers/portal.py
@@ -4,11 +4,12 @@
from collections import OrderedDict
from dateutil.relativedelta import relativedelta
from operator import itemgetter
+from datetime import datetime
from odoo import fields, http, _
from odoo.http import request
from odoo.tools import date_utils, groupby as groupbyelem
-from odoo.osv.expression import AND
+from odoo.osv.expression import AND, OR
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
@@ -22,23 +23,30 @@ class TimesheetCustomerPortal(CustomerPortal):
return values
@http.route(['/my/timesheets', '/my/timesheets/page/<int:page>'], type='http', auth="user", website=True)
- def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, search_in='all', groupby='project', **kw):
+ def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, search_in='all', groupby='none', **kw):
Timesheet_sudo = request.env['account.analytic.line'].sudo()
values = self._prepare_portal_layout_values()
domain = request.env['account.analytic.line']._timesheet_get_portal_domain()
searchbar_sortings = {
'date': {'label': _('Newest'), 'order': 'date desc'},
- 'name': {'label': _('Name'), 'order': 'name'},
+ 'name': {'label': _('Description'), 'order': 'name'},
}
searchbar_inputs = {
'all': {'input': 'all', 'label': _('Search in All')},
+ 'project': {'input': 'project', 'label': _('Search in Project')},
+ 'name': {'input': 'name', 'label': _('Search in Description')},
+ 'employee': {'input': 'employee', 'label': _('Search in Employee')},
+ 'task': {'input': 'task', 'label': _('Search in Task')},
}
searchbar_groupby = {
'none': {'input': 'none', 'label': _('None')},
'project': {'input': 'project', 'label': _('Project')},
+ 'task': {'input': 'task', 'label': _('Task')},
+ 'date': {'input': 'date', 'label': _('Date')},
+ 'employee': {'input': 'employee', 'label': _('Employee')},
}
today = fields.Date.today()
@@ -68,7 +76,16 @@ class TimesheetCustomerPortal(CustomerPortal):
domain = AND([domain, searchbar_filters[filterby]['domain']])
if search and search_in:
- domain = AND([domain, [('name', 'ilike', search)]])
+ search_domain = []
+ if search_in in ('project', 'all'):
+ search_domain = OR([search_domain, [('project_id', 'ilike', search)]])
+ if search_in in ('name', 'all'):
+ search_domain = OR([search_domain, [('name', 'ilike', search)]])
+ if search_in in ('employee', 'all'):
+ search_domain = OR([search_domain, [('employee_id', 'ilike', search)]])
+ if search_in in ('task', 'all'):
+ search_domain = OR([search_domain, [('task_id', 'ilike', search)]])
+ domain += search_domain
timesheet_count = Timesheet_sudo.search_count(domain)
# pager
@@ -80,13 +97,34 @@ class TimesheetCustomerPortal(CustomerPortal):
step=self._items_per_page
)
- if groupby == 'project':
- order = "project_id, %s" % order
- timesheets = Timesheet_sudo.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
- if groupby == 'project':
- grouped_timesheets = [Timesheet_sudo.concat(*g) for k, g in groupbyelem(timesheets, itemgetter('project_id'))]
- else:
- grouped_timesheets = [timesheets]
+ def get_timesheets():
+ groupby_mapping = {
+ 'project': 'project_id',
+ 'task': 'task_id',
+ 'employee': 'employee_id',
+ 'date': 'date',
+ }
+ field = groupby_mapping.get(groupby, None)
+ orderby = '%s, %s' % (field, order) if field else order
+ timesheets = Timesheet_sudo.search(domain, order=orderby, limit=self._items_per_page, offset=pager['offset'])
+ if field:
+ if groupby == 'date':
+ time_data = Timesheet_sudo.read_group(domain, ['date', 'unit_amount:sum'], ['date:day'])
+ mapped_time = dict([(datetime.strptime(m['date:day'], '%d %b %Y').date(), m['unit_amount']) for m in time_data])
+ grouped_timesheets = [(Timesheet_sudo.concat(*g), mapped_time[k]) for k, g in groupbyelem(timesheets, itemgetter('date'))]
+ else:
+ time_data = time_data = Timesheet_sudo.read_group(domain, [field, 'unit_amount:sum'], [field])
+ mapped_time = dict([(m[field][0], m['unit_amount']) for m in time_data])
+ grouped_timesheets = [(Timesheet_sudo.concat(*g), mapped_time[k.id]) for k, g in groupbyelem(timesheets, itemgetter(field))]
+ return timesheets, grouped_timesheets
+
+ grouped_timesheets = [(
+ timesheets,
+ sum(Timesheet_sudo.search(domain).mapped('unit_amount'))
+ )] if timesheets else []
+ return timesheets, grouped_timesheets
+
+ timesheets, grouped_timesheets = get_timesheets()
values.update({
'timesheets': timesheets,
diff --git a/addons/hr_timesheet/controllers/project.py b/addons/hr_timesheet/controllers/project.py
index bc321ab98fa..dd377160d6b 100644
--- a/addons/hr_timesheet/controllers/project.py
+++ b/addons/hr_timesheet/controllers/project.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from collections import defaultdict
from odoo.http import request
from odoo.osv import expression
@@ -12,7 +13,13 @@ class ProjectCustomerPortal(CustomerPortal):
def _task_get_page_view_values(self, task, access_token, **kwargs):
values = super(ProjectCustomerPortal, self)._task_get_page_view_values(task, access_token, **kwargs)
domain = request.env['account.analytic.line']._timesheet_get_portal_domain()
- domain = expression.AND([domain, [('task_id', '=', task.id)]])
- timesheets = request.env['account.analytic.line'].sudo().search(domain)
+ task_domain = expression.AND([domain, [('task_id', '=', task.id)]])
+ subtask_domain = expression.AND([domain, [('task_id', 'in', task.child_ids.ids)]])
+ timesheets = request.env['account.analytic.line'].sudo().search(task_domain)
+ subtasks_timesheets = request.env['account.analytic.line'].sudo().search(subtask_domain)
+ timesheets_by_subtask = defaultdict(lambda: request.env['account.analytic.line'].sudo())
+ for timesheet in subtasks_timesheets:
+ timesheets_by_subtask[timesheet.task_id] |= timesheet
values['timesheets'] = timesheets
+ values['timesheets_by_subtask'] = timesheets_by_subtask
return values
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index f8001201a57..4d55d5942ff 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -126,12 +126,12 @@ class AccountAnalyticLine(models.Model):
return etree.tostring(doc, encoding='unicode')
def _timesheet_get_portal_domain(self):
- return ['|', '&',
- ('task_id.project_id.privacy_visibility', '=', 'portal'),
- ('task_id.project_id.message_partner_ids', 'child_of', [self.env.user.partner_id.commercial_partner_id.id]),
- '&',
- ('task_id.project_id.privacy_visibility', '=', 'portal'),
- ('task_id.message_partner_ids', 'child_of', [self.env.user.partner_id.commercial_partner_id.id])]
+ return ['&',
+ '|', '|',
+ ('task_id.project_id.message_partner_ids', 'child_of', [self.env.user.partner_id.commercial_partner_id.id]),
+ ('task_id.message_partner_ids', 'child_of', [self.env.user.partner_id.commercial_partner_id.id]),
+ ('task_id.project_id.allowed_portal_user_ids', 'child_of', [self.env.user.id]),
+ ('task_id.project_id.privacy_visibility', '=', 'portal')]
def _timesheet_preprocess(self, vals):
""" Deduce other field values from the one given.
diff --git a/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml b/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml
index cc0ec4d8f04..3d0044a2ea9 100644
--- a/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml
+++ b/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml
@@ -33,24 +33,45 @@
</t>
<t t-if="grouped_timesheets">
<t t-call="portal.portal_table">
- <t t-foreach="grouped_timesheets" t-as="timesheets">
+ <t t-foreach="grouped_timesheets" t-as="timesheets_with_hours">
+ <t t-set="timesheets" t-value="timesheets_with_hours[0]"/>
+ <t t-set="hours_spent" t-value="timesheets_with_hours[1]"/>
<thead>
- <tr t-attf-class="{{'thead-light' if not groupby == 'none' else ''}}">
- <th t-if="groupby == 'none'">Description</th>
- <th t-else="">
+ <tr t-if="not groupby =='none'" t-attf-class="{{'thead-light'}}">
+ <th t-if="groupby == 'project'" colspan="5">
<em class="font-weight-normal text-muted">Timesheets for project:</em>
- <span t-field="timesheets[0].project_id.name"/></th>
- <th>Date</th>
- <th>Employee</th>
- <th class="text-right">Duration</th>
+ <span t-field="timesheets[0].project_id.name"/>
+ </th>
+ <th t-elif="groupby == 'task'" colspan="5">
+ <em class="font-weight-normal text-muted">Timesheets for task:</em>
+ <span t-field="timesheets[0].task_id.name"/>
+ </th>
+ <th t-elif="groupby == 'date'" colspan="5">
+ <em class="font-weight-normal text-muted">Timesheets on </em>
+ <span t-field="timesheets[0].date"/>
+ </th>
+ <th t-elif="groupby == 'employee'" colspan="5">
+ <em class="font-weight-normal text-muted">Timesheets for employee:</em>
+ <span t-field="timesheets[0].employee_id.name"/>
+ </th>
+ </tr>
+ <tr>
+ <th t-if="not groupby == 'date'">Date</th>
+ <th t-if="not groupby == 'employee'">Employee</th>
+ <th t-if="not groupby == 'project'">Project</th>
+ <th t-if="not groupby == 'task'">Task</th>
+ <th>Description</th>
+ <th class="text-right">Hours Spent <small class="text-muted">(Total: <span t-esc="hours_spent" t-options='{"widget": "float_time"}'/>)</small></th>
</tr>
</thead>
<tbody>
<t t-foreach="timesheets" t-as="timesheet">
<tr>
+ <td t-if="not groupby == 'date'"><span t-field="timesheet.date" t-options='{"widget": "date"}'/></td>
+ <td t-if="not groupby == 'employee'"><span t-field="timesheet.employee_id"/></td>
+ <td t-if="not groupby == 'project'"><span t-field="timesheet.project_id"/></td>
+ <td t-if="not groupby == 'task'"><span t-field="timesheet.task_id"/></td>
<td><span t-esc="timesheet.name"/></td>
- <td><span t-field="timesheet.date" t-options='{"widget": "date"}'/></td>
- <td><span t-field="timesheet.employee_id"/></td>
<td class="text-right"><span t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/></td>
</tr>
</t>
diff --git a/addons/hr_timesheet/views/project_portal_templates.xml b/addons/hr_timesheet/views/project_portal_templates.xml
index 0d248cd1eed..19c0648db03 100644
--- a/addons/hr_timesheet/views/project_portal_templates.xml
+++ b/addons/hr_timesheet/views/project_portal_templates.xml
@@ -8,6 +8,9 @@
<h5 class="mt-2 mb-2">Timesheets</h5>
<t t-call="hr_timesheet.portal_timesheet_table"/>
</div>
+ <t t-if="timesheets_by_subtask">
+ <t t-call="hr_timesheet.portal_subtask_timesheet_tables"/>
+ </t>
</xpath>
</template>
@@ -16,18 +19,58 @@
<thead>
<tr>
<th>Date</th>
- <th>Description</th>
<th>Employee</th>
+ <th>Description</th>
<th class="text-right">Duration</th>
</tr>
</thead>
<tr t-foreach="timesheets" t-as="timesheet">
<td><t t-esc="timesheet.date" t-options='{"widget": "date"}'/></td>
- <td><t t-esc="timesheet.name"/></td>
<td><t t-esc="timesheet.employee_id.name"/></td>
+ <td><t t-esc="timesheet.name"/></td>
<td class="text-right"><span t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/></td>
</tr>
+ <tfoot>
+ <tr>
+ <th colspan="3"></th>
+ <th class="text-right">Total Hours: <span t-esc="round(sum(timesheets.mapped('unit_amount')), 2)" t-options='{"widget": "float_time"}'/></th>
+ </tr>
+ </tfoot>
</table>
</template>
+ <template id="portal_subtask_timesheet_tables" name="Portal Subtask Timesheet Tables">
+ <t t-foreach="timesheets_by_subtask.values()" t-as="timesheets">
+ <t t-set="total_hours" t-value="round(sum(timesheets.mapped('unit_amount')), 2)"/>
+ <t t-if="total_hours &gt; 0">
+ <div class="container">
+ <hr class="mt-4 mb-1"/>
+ <h5 class="mt-2 mb-2">Timesheets for sub-task: <t t-esc="timesheets[0].task_id.name" /></h5>
+ <table class="table table-sm">
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Employee</th>
+ <th>Description</th>
+ <th class="text-right">Duration</th>
+ </tr>
+ </thead>
+ <tr t-foreach="timesheets" t-as="timesheet">
+ <td><t t-esc="timesheet.date" t-options='{"widget": "date"}'/></td>
+ <td><t t-esc="timesheet.employee_id.name"/></td>
+ <td><t t-esc="timesheet.name"/></td>
+ <td class="text-right"><span t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/></td>
+ </tr>
+ <tfoot>
+ <tr>
+ <th class="text-right" colspan="3"></th>
+ <th class="text-right">Total Hours: <span t-esc="total_hours" t-options='{"widget": "float_time"}'/></th>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </t>
+ </t>
+ </template>
+
</odoo>
diff --git a/addons/mail/static/src/js/basic_view.js b/addons/mail/static/src/js/basic_view.js
index 042e0ded8e8..6d74903e19c 100644
--- a/addons/mail/static/src/js/basic_view.js
+++ b/addons/mail/static/src/js/basic_view.js
@@ -9,12 +9,14 @@ BasicView.include({
init: function () {
this._super.apply(this, arguments);
const post_refresh = this._getFieldOption('message_ids', 'post_refresh', false);
+ const followers_post_refresh = this._getFieldOption('message_follower_ids', 'post_refresh', false);
this.chatterFields = {
hasActivityIds: this._hasField('activity_ids'),
hasMessageFollowerIds: this._hasField('message_follower_ids'),
hasMessageIds: this._hasField('message_ids'),
hasRecordReloadOnAttachmentsChanged: post_refresh === 'always',
hasRecordReloadOnMessagePosted: !!post_refresh,
+ hasRecordReloadOnFollowersUpdate: !!followers_post_refresh,
isActivityBoxVisible: this._getFieldOption('message_ids', 'open_attachments', false),
};
const fieldsInfo = this.fieldsInfo[this.viewType];
diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js
index 2267d170f10..9bedc8096bc 100644
--- a/addons/mail/static/src/models/thread/thread.js
+++ b/addons/mail/static/src/models/thread/thread.js
@@ -1045,6 +1045,7 @@ function factory(dependencies) {
* @param {boolean} [param0.mail_invite_follower_channel_only=false]
*/
_promptAddFollower({ mail_invite_follower_channel_only = false } = {}) {
+ const self = this;
const action = {
type: 'ir.actions.act_window',
res_model: 'mail.wizard.invite',
@@ -1061,7 +1062,10 @@ function factory(dependencies) {
this.env.bus.trigger('do-action', {
action,
options: {
- on_close: () => this.refreshFollowers(),
+ on_close: async () => {
+ await this.async(() => this.refreshFollowers());
+ this.env.bus.trigger('mail.thread:promptAddFollower-closed');
+ },
},
});
}
diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js
index df9c8b90d25..52f2d962ba2 100644
--- a/addons/mail/static/src/widgets/form_renderer/form_renderer.js
+++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js
@@ -40,6 +40,7 @@ FormRenderer.include({
this.off('o_attachments_changed', this);
this.off('o_chatter_rendered', this);
this.off('o_message_posted', this);
+ owl.Component.env.bus.off('mail.thread:promptAddFollower-closed', this);
},
//--------------------------------------------------------------------------
@@ -75,6 +76,9 @@ FormRenderer.include({
if (this.chatterFields.hasRecordReloadOnAttachmentsChanged) {
this.on('o_attachments_changed', this, ev => this.trigger_up('reload'));
}
+ if (this.chatterFields.hasRecordReloadOnFollowersUpdate) {
+ owl.Component.env.bus.on('mail.thread:promptAddFollower-closed', this, ev => this.trigger_up('reload'));
+ }
},
/**
* @private
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 4ab3f1380b3..498aa9a96f3 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -341,6 +341,8 @@ class Project(models.Model):
if allowed_users_changed:
for project in self:
permission_removed = allowed_users.get(project) - project.allowed_user_ids
+ allowed_portal_users_removed = permission_removed.filtered('share')
+ project.message_unsubscribe(allowed_portal_users_removed.partner_id.commercial_partner_id.ids)
for task in project.task_ids:
task.allowed_user_ids -= permission_removed
@@ -384,13 +386,18 @@ class Project(models.Model):
return result
def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None):
- """ Subscribe to all existing active tasks when subscribing to a project """
+ """
+ Subscribe to all existing active tasks when subscribing to a project
+ And add the portal user subscribed to allowed portal users
+ """
res = super(Project, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
project_subtypes = self.env['mail.message.subtype'].browse(subtype_ids) if subtype_ids else None
task_subtypes = project_subtypes.mapped('parent_id').ids if project_subtypes else None
if not subtype_ids or task_subtypes:
self.mapped('tasks').message_subscribe(
partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=task_subtypes)
+ if partner_ids:
+ self.allowed_portal_user_ids |= self.env['res.partner'].browse(partner_ids).user_ids.filtered('share')
return res
def message_unsubscribe(self, partner_ids=None, channel_ids=None):
@@ -780,6 +787,17 @@ class Task(models.Model):
)
return super(Task, self).get_empty_list_help(help)
+ def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None):
+ """
+ Add the users subscribed to allowed portal users
+ """
+ res = super(Task, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
+ if partner_ids:
+ new_allowed_users = self.env['res.partner'].browse(partner_ids).user_ids.filtered('share')
+ tasks = self.filtered(lambda task: task.project_id.privacy_visibility == 'portal')
+ tasks.write({'allowed_user_ids': [(4, user.id) for user in new_allowed_users]})
+ return res
+
# ----------------------------------------
# Case management
# ----------------------------------------
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index f452d6fb870..a3902381fa4 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -357,7 +357,7 @@
</page>
</notebook>
<div class="oe_chatter">
- <field name="message_follower_ids" help="Follow this project to automatically track the events associated to tasks and issues of this project." groups="base.group_user"/>
+ <field name="message_follower_ids" options="{'post_refresh':True}" help="Follow this project to automatically track the events associated to tasks and issues of this project." groups="base.group_user"/>
</div>
</sheet>
</form>
@@ -695,7 +695,7 @@
</notebook>
</sheet>
<div class="oe_chatter">
- <field name="message_follower_ids" groups="base.group_user"/>
+ <field name="message_follower_ids" options="{'post_refresh':True}" groups="base.group_user"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py
index c4d5868918a..44885e0b71a 100644
--- a/addons/sale_project/models/sale_order.py
+++ b/addons/sale_project/models/sale_order.py
@@ -66,6 +66,7 @@ class SaleOrder(models.Model):
if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...)
action = self.with_context(active_id=task_projects.id).env.ref(
'project.act_project_project_2_project_task_all').read()[0]
+ action['domain'] = [('project_id', '=', task_projects.id)]
if action.get('context'):
eval_context = self.env['ir.actions.actions']._get_eval_context()
eval_context.update({'active_id': task_projects.id})
diff --git a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml
index e5127f88fea..d7ade138475 100644
--- a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml
+++ b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml
@@ -26,7 +26,28 @@
</h5>
</div>
<div id="collapseTimesheet" class="card-body show" data-parent="#accordion">
- <t t-call="hr_timesheet.portal_timesheet_table"/>
+ <t t-set="nr_tasks" t-value="len(timesheets.mapped('task_id'))"/>
+ <t t-set="nr_projects" t-value="len(timesheets.mapped('project_id'))"/>
+ <table class="table table-sm">
+ <thead>
+ <tr>
+ <th>Date</th>
+ <th>Employee</th>
+ <th t-if="nr_projects &gt; 1">Project</th>
+ <th t-if="nr_tasks &gt; 0">Task</th>
+ <th>Description</th>
+ <th class="text-right">Duration</th>
+ </tr>
+ </thead>
+ <tr t-foreach="timesheets" t-as="timesheet">
+ <td><t t-esc="timesheet.date" t-options='{"widget": "date"}'/></td>
+ <td><t t-esc="timesheet.employee_id.name"/></td>
+ <td t-if="nr_projects &gt; 1"><span t-field="timesheet.project_id"/></td>
+ <td t-if="nr_tasks &gt; 0"><span t-field="timesheet.task_id"/></td>
+ <td><t t-esc="timesheet.name"/></td>
+ <td class="text-right"><span t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/></td>
+ </tr>
+ </table>
</div>
</div>
</div>