diff options
| author | qho <qho@odoo.com> | 2020-03-10 07:50:27 +0000 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2020-06-29 11:56:36 +0000 |
| commit | 1424a3839a6c304599bdeefcd74f29279ae75420 (patch) | |
| tree | f78d9388f3dfd55ffdc8b24ac6aaa4304571fd94 | |
| parent | 78e2d2e2fb49814c2f51dd97d2ddba6ba533d2e1 (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.py | 60 | ||||
| -rw-r--r-- | addons/hr_timesheet/controllers/project.py | 11 | ||||
| -rw-r--r-- | addons/hr_timesheet/models/hr_timesheet.py | 12 | ||||
| -rw-r--r-- | addons/hr_timesheet/views/hr_timesheet_portal_templates.xml | 41 | ||||
| -rw-r--r-- | addons/hr_timesheet/views/project_portal_templates.xml | 47 | ||||
| -rw-r--r-- | addons/mail/static/src/js/basic_view.js | 2 | ||||
| -rw-r--r-- | addons/mail/static/src/models/thread/thread.js | 6 | ||||
| -rw-r--r-- | addons/mail/static/src/widgets/form_renderer/form_renderer.js | 4 | ||||
| -rw-r--r-- | addons/project/models/project.py | 20 | ||||
| -rw-r--r-- | addons/project/views/project_views.xml | 4 | ||||
| -rw-r--r-- | addons/sale_project/models/sale_order.py | 1 | ||||
| -rw-r--r-- | addons/sale_timesheet/views/sale_timesheet_portal_templates.xml | 23 |
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 > 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 > 1">Project</th> + <th t-if="nr_tasks > 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 > 1"><span t-field="timesheet.project_id"/></td> + <td t-if="nr_tasks > 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> |
