From b0f165bbd8041b5a01fb09122da76ba14cfa3e94 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Mon, 30 Nov 2020 18:51:36 +0100 Subject: [PATCH 01/41] [IMP] sale_timesheet: remaining hours in task sales_line_id Prior to this commit: - The sale_line_id name_get shows the product name. After this commit: - In the task Form view, the SOL also displays the remaining quantity for products which have a service_policy set to 'ordered_timesheet'. task-2409761 --- addons/sale_timesheet/models/project.py | 22 +++++++ addons/sale_timesheet/models/sale_order.py | 63 ++++++++++++++++++- .../views/project_task_views.xml | 17 ++++- 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index a09a31d44d0d6..c51f5adf374c7 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -231,6 +231,28 @@ def default_get(self, fields): # TODO: [XBO] remove me in master non_allow_billable = fields.Boolean("Non-Billable", help="Your timesheets linked to this task will not be billed.") + remaining_hours_so = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours_so') + remaining_hours_available = fields.Boolean(related="sale_line_id.remaining_hours_available") + + @api.depends('sale_line_id', 'timesheet_ids', 'timesheet_ids.unit_amount') + def _compute_remaining_hours_so(self): + # TODO This is not yet perfectly working as timesheet.so_line stick to its old value although changed + # in the task From View. + timesheets = self.timesheet_ids.filtered(lambda t: t.task_id.sale_line_id in (t.so_line, t._origin.so_line) and t.so_line.remaining_hours_available) + + mapped_remaining_hours = {task._origin.id: task.sale_line_id and task.sale_line_id.remaining_hours or 0.0 for task in self} + uom_hour = self.env.ref('uom.product_uom_hour') + for timesheet in timesheets: + delta = 0 + if timesheet._origin.so_line == timesheet.task_id.sale_line_id: + delta += timesheet._origin.unit_amount + if timesheet.so_line == timesheet.task_id.sale_line_id: + delta -= timesheet.unit_amount + if delta: + mapped_remaining_hours[timesheet.task_id._origin.id] += timesheet.so_line.product_uom._compute_quantity(delta, uom_hour) + + for task in self: + task.remaining_hours_so = mapped_remaining_hours[task._origin.id] @api.depends( 'allow_billable', 'allow_timesheets', 'sale_order_id') diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index e6d13265f36ed..af066c44cd115 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ from odoo.osv import expression +import math class SaleOrder(models.Model): @@ -78,6 +79,66 @@ class SaleOrderLine(models.Model): qty_delivered_method = fields.Selection(selection_add=[('timesheet', 'Timesheets')]) analytic_line_ids = fields.One2many(domain=[('project_id', '=', False)]) # only analytic lines, not timesheets (since this field determine if SO line came from expense) + remaining_hours_available = fields.Boolean(compute='_compute_remaining_hours_available') + remaining_hours = fields.Float('Remaining Hours on SO', compute='_compute_remaining_hours') + + def name_get(self): + res = super(SaleOrderLine, self).name_get() + if self.env.context.get('with_remaining_hours'): + names = dict(res) + result = [] + uom_hour = self.env.ref('uom.product_uom_hour') + uom_day = self.env.ref('uom.product_uom_day') + for line in self: + name = names.get(line.id) + if line.remaining_hours_available: + company = self.env.company + encoding_uom = company.timesheet_encode_uom_id + remaining_time = '' + if encoding_uom == uom_hour: + hours, minutes = divmod(abs(line.remaining_hours) * 60, 60) + round_minutes = minutes / 30 + minutes = math.ceil(round_minutes) if line.remaining_hours >= 0 else math.floor(round_minutes) + if minutes > 1: + minutes = 0 + hours += 1 + else: + minutes = minutes * 30 + remaining_time =' ({sign}{hours:02.0f}:{minutes:02.0f})'.format( + sign='-' if line.remaining_hours < 0 else '', + hours=hours, + minutes=minutes) + elif encoding_uom == uom_day: + remaining_days = company.project_time_mode_id._compute_quantity(line.remaining_hours, encoding_uom, round=False) + remaining_time = ' ({qty:.02f} {unit})'.format( + qty=remaining_days, + unit=_('days') if abs(remaining_days) > 1 else _('day') + ) + name = '{name}{remaining_time}'.format( + name=name, + remaining_time=remaining_time + ) + result.append((line.id, name)) + return result + return res + + @api.depends('product_id.service_policy') + def _compute_remaining_hours_available(self): + uom_hour = self.env.ref('uom.product_uom_hour') + for line in self: + is_ordered_timesheet = line.product_id.service_policy == 'ordered_timesheet' + is_time_product = line.product_uom.category_id == uom_hour.category_id + line.remaining_hours_available = is_ordered_timesheet and is_time_product + + @api.depends('qty_delivered', 'product_uom_qty', 'analytic_line_ids') + def _compute_remaining_hours(self): + uom_hour = self.env.ref('uom.product_uom_hour') + for line in self: + remaining_hours = None + if line.remaining_hours_available: + qty_left = line.product_uom_qty - line.qty_delivered + remaining_hours = line.product_uom._compute_quantity(qty_left, uom_hour) + line.remaining_hours = remaining_hours @api.depends('product_id') def _compute_qty_delivered_method(self): diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index b75a28ed748fa..b3e7ac360d827 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -136,7 +136,7 @@ - + @@ -148,10 +148,25 @@ + {'with_remaining_hours': True} {'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]} + + + + + + From 1f27728ad717b9f70b19159b69e088fdab1d1777 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Mon, 30 Nov 2020 22:00:25 +0100 Subject: [PATCH 02/41] [IMP] sale_timesheet: remove internal reference from services demo data task-2409761 --- addons/event_sale/data/event_sale_data.xml | 1 - addons/event_sale/models/event_ticket.py | 2 -- .../event_sale/static/tests/tours/event_configurator_ui.js | 6 +++--- addons/hr_expense/data/hr_expense_data.xml | 1 - addons/hr_expense/data/hr_expense_demo.xml | 2 -- addons/product/data/product_demo.xml | 2 -- addons/sale/data/sale_demo.xml | 1 - addons/sale_timesheet/data/sale_service_demo.xml | 4 ---- 8 files changed, 3 insertions(+), 16 deletions(-) diff --git a/addons/event_sale/data/event_sale_data.xml b/addons/event_sale/data/event_sale_data.xml index bf8bc78249ead..725bc51f75719 100644 --- a/addons/event_sale/data/event_sale_data.xml +++ b/addons/event_sale/data/event_sale_data.xml @@ -14,7 +14,6 @@ Event Registration - EVENT_REG service diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py index 7759a547d8428..da142f973c9e4 100644 --- a/addons/event_sale/models/event_ticket.py +++ b/addons/event_sale/models/event_ticket.py @@ -72,8 +72,6 @@ def _init_column(self, column_name): 'list_price': 0, 'standard_price': 0, 'type': 'service', - 'default_code': 'EVENT_REG', - 'type': 'service', }).id self.env['ir.model.data'].create({ 'name': 'product_product_event', diff --git a/addons/event_sale/static/tests/tours/event_configurator_ui.js b/addons/event_sale/static/tests/tours/event_configurator_ui.js index 1170a142a0936..a7419871bbecc 100644 --- a/addons/event_sale/static/tests/tours/event_configurator_ui.js +++ b/addons/event_sale/static/tests/tours/event_configurator_ui.js @@ -20,10 +20,10 @@ tour.register('event_configurator_tour', { }, { trigger: 'div[name="product_id"] input, div[name="product_template_id"] input', run: function (actions) { - actions.text('EVENT'); + actions.text('Event'); } }, { - trigger: 'ul.ui-autocomplete a:contains("EVENT")', + trigger: 'ul.ui-autocomplete a:contains("Event")', run: 'click' }, { trigger: 'div[name="event_id"] input', @@ -56,7 +56,7 @@ tour.register('event_configurator_tour', { trigger: 'ul.nav a:contains("Order Lines")', run: 'click' }, { - trigger: 'td:contains("EVENT")', + trigger: 'td:contains("Event")', run: 'click' }, { trigger: '.o_edit_product_configuration' diff --git a/addons/hr_expense/data/hr_expense_data.xml b/addons/hr_expense/data/hr_expense_data.xml index 5133dc6d8a848..5005c238c44ae 100644 --- a/addons/hr_expense/data/hr_expense_data.xml +++ b/addons/hr_expense/data/hr_expense_data.xml @@ -6,7 +6,6 @@ 0.0 1.0 service - EXP_GEN diff --git a/addons/hr_expense/data/hr_expense_demo.xml b/addons/hr_expense/data/hr_expense_demo.xml index f881e5a646849..5ce0198e0e4aa 100644 --- a/addons/hr_expense/data/hr_expense_demo.xml +++ b/addons/hr_expense/data/hr_expense_demo.xml @@ -37,7 +37,6 @@ 0.32 service Car Travel Expenses - EXP_CT @@ -50,7 +49,6 @@ 700.0 service Air Flight - EXP_AF diff --git a/addons/product/data/product_demo.xml b/addons/product/data/product_demo.xml index 53d42a00af635..ba80566393ef6 100644 --- a/addons/product/data/product_demo.xml +++ b/addons/product/data/product_demo.xml @@ -37,7 +37,6 @@ 14.0 8.0 service - EXP_REST @@ -46,7 +45,6 @@ 400.0 400.0 service - EXP_HA diff --git a/addons/sale/data/sale_demo.xml b/addons/sale/data/sale_demo.xml index d1026815fdaa6..695736aae0edb 100644 --- a/addons/sale/data/sale_demo.xml +++ b/addons/sale/data/sale_demo.xml @@ -687,7 +687,6 @@ Thanks! Deposit - Deposit service 150.0 diff --git a/addons/sale_timesheet/data/sale_service_demo.xml b/addons/sale_timesheet/data/sale_service_demo.xml index f6aae634c6392..0b64fa6533704 100644 --- a/addons/sale_timesheet/data/sale_service_demo.xml +++ b/addons/sale_timesheet/data/sale_service_demo.xml @@ -45,7 +45,6 @@ Customer Care (Prepaid Hours) - SERV_585189 service 250.00 @@ -59,7 +58,6 @@ Senior Architect (Invoice on Timesheets) - SERV_89744 200.00 150.00 @@ -72,7 +70,6 @@ Junior Architect (Invoice on Timesheets) - SERV_89665 100.00 85.00 @@ -85,7 +82,6 @@ Kitchen Assembly (Milestones) - SERV_32289 500 420.00 From eb50424897581f1378de155b184b3045bf222d76 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Tue, 1 Dec 2020 14:38:55 +0100 Subject: [PATCH 03/41] [IMP] sale_timesheet: always display sol in project timesheets list task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index b3e7ac360d827..ec3b928291ce5 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -136,7 +136,7 @@ - + From 11d2da544f2991bd7db3ceccc0a2589f57af9d9b Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Tue, 1 Dec 2020 15:23:48 +0100 Subject: [PATCH 04/41] [IMP] sale_timesheet: hide non_allow_billable Remove non_allow_billable as it has been removed from project.project in PR odoo/odoo#62360. We still display the field in the form view in order to let the user unsetting this field when it is set to true. For the other way around, the user need to set the so_line to false. task-2409761 --- addons/sale_timesheet/views/hr_timesheet_views.xml | 6 +++--- addons/sale_timesheet/views/project_task_views.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/sale_timesheet/views/hr_timesheet_views.xml b/addons/sale_timesheet/views/hr_timesheet_views.xml index 31a02c6eedb46..b3f34e8db5c80 100644 --- a/addons/sale_timesheet/views/hr_timesheet_views.xml +++ b/addons/sale_timesheet/views/hr_timesheet_views.xml @@ -10,8 +10,8 @@ - - + + @@ -42,7 +42,7 @@ - + diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index ec3b928291ce5..5f7e9ee69200e 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -137,7 +137,7 @@ - + From 8052da9602437c8de03bd7f675b7086500fcdb10 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Wed, 2 Dec 2020 16:24:06 +0100 Subject: [PATCH 05/41] [IMP] sale_project: only recompute planned_hours for service product task-2409761 --- addons/sale_project/models/sale_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index e779902dcc4b0..f974dbefabeb3 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -155,7 +155,7 @@ def write(self, values): # of a locked sale order. if 'product_uom_qty' in values and not self.env.context.get('no_update_planned_hours', False): for line in self: - if line.task_id: + if line.task_id and line.product_id.type == 'service': planned_hours = line._convert_qty_company_hours(line.task_id.company_id) line.task_id.write({'planned_hours': planned_hours}) return result From 16a1c202bd30450599da1834f6af8f1cd71b6343 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Fri, 4 Dec 2020 09:40:31 +0100 Subject: [PATCH 06/41] [IMP] sale_project: invert SO and SOL order in task form As the sale_line_id field has a domain that depends on the sale_order_id, it make more sense to have the user first deciding what to do with the sale_order_id before going to the sale_line_id one. task-2409761 --- addons/sale_project/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index a779fea42e139..436eeb0b02060 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -27,8 +27,8 @@ groups="sales_team.group_sale_salesman"/> - + From ec9de7acc0d78864e33d92ab135a84b9254eba06 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Fri, 4 Dec 2020 15:47:51 +0100 Subject: [PATCH 07/41] [IMP] sale_timesheet: show all timesheets linked to the SO on portal task-2409761 --- addons/sale_timesheet/models/account.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index e946c30d18d58..ddc68509b6cfa 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -137,6 +137,9 @@ def _timesheet_get_portal_domain(self): @api.model def _timesheet_get_sale_domain(self, order_lines_ids, invoice_ids): + if not invoice_ids: + return [('so_line', 'in', order_lines_ids.ids)] + return [ '|', '&', From 89e435facbeef102a2df32113050cf09b635cc03 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 30 Nov 2020 18:07:44 +0100 Subject: [PATCH 08/41] [FIX] sale_timesheet: add the default timesheet product in project When the user checks the allow_billable and save, we have an validation error because the timesheet_product_id=False. In fact, the compute method to correctly set on the project model but not in the definition of the field. This commit add the compute method in the field and keep the previous behaviour. That is, we keep this field as store and editable field. task-2409761 --- addons/sale_timesheet/models/project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index c51f5adf374c7..db984cfd22d04 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -47,6 +47,7 @@ def _default_timesheet_product_id(self): ('service_type', '=', 'timesheet'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]""", help='Select a Service product with which you would like to bill your time spent on tasks.', + compute="_compute_timesheet_product_id", store=True, readonly=False, default=_default_timesheet_product_id) warning_employee_rate = fields.Boolean(compute='_compute_warning_employee_rate') From c02acc394febcc855085c9bd49557404047c7fe9 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 1 Dec 2020 13:16:59 +0100 Subject: [PATCH 09/41] [IMP] sale_timesheet: filter directly the timesheets Before this commit, we take all timesheets in the loop and we filter these inside the loop. This commit filters before to enter in the loop. task-2409761 --- addons/sale_timesheet/models/account.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index ddc68509b6cfa..88160ecdda027 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -57,10 +57,9 @@ def _onchange_task_id_employee_id(self): @api.constrains('so_line', 'project_id') def _check_sale_line_in_project_map(self): - for timesheet in self: - if timesheet.project_id and timesheet.so_line: # billed timesheet - if timesheet.so_line not in timesheet.project_id.mapped('sale_line_employee_ids.sale_line_id') | timesheet.task_id.sale_line_id | timesheet.project_id.sale_line_id: - raise ValidationError(_("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line.")) + for timesheet in self.filtered(lambda t: t.project_id and t.so_line): # billed timesheet + if timesheet.so_line not in timesheet.project_id.mapped('sale_line_employee_ids.sale_line_id') | timesheet.task_id.sale_line_id | timesheet.project_id.sale_line_id: + raise ValidationError(_("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line.")) def write(self, values): # prevent to update invoiced timesheets if one line is of type delivery From 527d854a4b8b71b5f38d524766834ea5124e9093 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 1 Dec 2020 16:07:43 +0100 Subject: [PATCH 10/41] [IMP] sale_timesheet: split the check in two methods This commit splits the _check_sale_line_in_project_map method in two methods to easily override the condition in the constrains (useful to accept the tickets where no SOL is defined in project linked in helpdesk team) task-2409761 --- addons/sale_timesheet/models/account.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 88160ecdda027..deae4c5ca8797 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -55,11 +55,13 @@ def _onchange_task_id_employee_id(self): else: self.so_line = False + def _check_timesheet_can_be_billed(self): + return self.so_line in self.project_id.mapped('sale_line_employee_ids.sale_line_id') | self.task_id.sale_line_id | self.project_id.sale_line_id + @api.constrains('so_line', 'project_id') def _check_sale_line_in_project_map(self): - for timesheet in self.filtered(lambda t: t.project_id and t.so_line): # billed timesheet - if timesheet.so_line not in timesheet.project_id.mapped('sale_line_employee_ids.sale_line_id') | timesheet.task_id.sale_line_id | timesheet.project_id.sale_line_id: - raise ValidationError(_("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line.")) + if not all(t._check_timesheet_can_be_billed() for t in self.filtered(lambda t: t.project_id and t.so_line)): + raise ValidationError(_("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line.")) def write(self, values): # prevent to update invoiced timesheets if one line is of type delivery From 68f72c6db5dbe10b7f1ee42dd149db2d9dd978fc Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Fri, 4 Dec 2020 10:47:04 +0100 Subject: [PATCH 11/41] [FIX] sale_timesheet: determine the correct SOL for timesheet Before this commit, when we have not any task in timesheet and the projet has not any sale_line_id, the mapping for the project configured as employee rate is not done and return an empty sale order line for the timesheet. Moreover, if we have a task, the project of this task can be different of the project of the timesheet. That's why, the sale order line determined from the project of the task is not a good idea and thus the python constrains called _check_sale_line_in_project_map can be not satisfied. In this commit, we change the condition when there is not task in the timesheet to get the correct sale order line when pricing type of the project is "employee rate". And we now use the project of the timesheet to have the correct SOL for the timesheet. task-2409761 --- addons/sale_timesheet/models/account.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index deae4c5ca8797..dde88bc389caa 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -109,19 +109,20 @@ def _timesheet_determine_sale_line(self, task, employee, project): 2/ timesheet on employee rate task: find the SO line in the map of the project (even for subtask), or fallback on the SO line of the task, or fallback on the one on the project """ - if project.sale_line_id and not task: + if not task: if project.bill_type == 'customer_project' and project.pricing_type == 'employee_rate': map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', project.id), ('employee_id', '=', employee.id)]) if map_entry: return map_entry.sale_line_id - return project.sale_line_id + if project.sale_line_id: + return project.sale_line_id if task.allow_billable: if task.bill_type == 'customer_task': return task.sale_line_id if task.pricing_type == 'fixed_rate': return task.sale_line_id elif task.pricing_type == 'employee_rate' and not task.non_allow_billable: - map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', task.project_id.id), ('employee_id', '=', employee.id)]) + map_entry = project.sale_line_employee_ids.filtered(lambda map_entry: map_entry.employee_id == employee) if map_entry: return map_entry.sale_line_id if task.sale_line_id or project.sale_line_id: From 2b1eec5c86ed26a0fb9317d29c0c4eec6186cf6e Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Wed, 2 Dec 2020 17:12:08 +0100 Subject: [PATCH 12/41] [FIX] sale_timesheet: changes the constrains + compute sol Before this commit, the constrains checks all timesheets if the SOL corresponds to an sol in task/project/map entry. The problem with this, it is for the timesheets which have already been invoiced, we cannot changes their sol. The calculation to determine the correct sol to a timesheet not yet invoiced is made in the preprocess and postprecess in create and write method of timesheet. The first problem is the postprecess does not look if the sol computed in preprocess is correct and erase to put the sol computed in this method. The second one is when the user add a line in task or a ticket, the sol is empty and not compute. The user needs to save to trigger the create/write methods and see the sol given in the timesheets created. In the idea, if the user changes the employee of a timesheet, the sol is not directly computed to have the sol in map entry of this new employee (if the pricing type of the project linked is employee rate). In this commit, we filter the timesheets in the constrains to have only the timesheet which have not already been invoiced. We also add a compute method in the so_line field of timesheet, this compute method will replace the works of the preprocess and postprocess explained above. With this compute, we can determine directly the correct sol for the timesheet when a change is made by the user. task-2409761 --- addons/sale_timesheet/models/account.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index dde88bc389caa..5d2c503e9b1df 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -22,7 +22,9 @@ def _default_sale_line_domain(self): ('non_billable_project', 'No task found')], string="Billable Type", compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True) timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet") non_allow_billable = fields.Boolean("Non-Billable", help="Your timesheet will not be billed.") + so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False) + # TODO: [XBO] Since the task_id is not required in this model, then it should more efficient to depends to bill_type and pricing_type of project (See in master) @api.depends('so_line.product_id', 'project_id', 'task_id', 'non_allow_billable', 'task_id.bill_type', 'task_id.pricing_type', 'task_id.non_allow_billable') def _compute_timesheet_invoice_type(self): non_allowed_billable = self.filtered('non_allow_billable') @@ -55,12 +57,20 @@ def _onchange_task_id_employee_id(self): else: self.so_line = False + @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'employee_id') + def _compute_so_line(self): + for timesheet in self._get_not_billed(): # Get only the timesheets are not yet invoiced + timesheet.so_line = timesheet._timesheet_determine_sale_line(timesheet.task_id, timesheet.employee_id, timesheet.project_id) + + def _get_not_billed(self): + return self.filtered(lambda t: not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') + def _check_timesheet_can_be_billed(self): return self.so_line in self.project_id.mapped('sale_line_employee_ids.sale_line_id') | self.task_id.sale_line_id | self.project_id.sale_line_id @api.constrains('so_line', 'project_id') def _check_sale_line_in_project_map(self): - if not all(t._check_timesheet_can_be_billed() for t in self.filtered(lambda t: t.project_id and t.so_line)): + if not all(t._check_timesheet_can_be_billed() for t in self._get_not_billed().filtered(lambda t: t.project_id and t.so_line)): raise ValidationError(_("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line.")) def write(self, values): @@ -92,16 +102,6 @@ def _timesheet_preprocess(self, values): values['so_line'] = self._timesheet_determine_sale_line(task, employee, project).id return values - def _timesheet_postprocess_values(self, values): - result = super(AccountAnalyticLine, self)._timesheet_postprocess_values(values) - # (re)compute the sale line - if any(field_name in values for field_name in ['task_id', 'employee_id', 'project_id']): - for timesheet in self: - result[timesheet.id].update({ - 'so_line': timesheet._timesheet_determine_sale_line(timesheet.task_id, timesheet.employee_id, timesheet.project_id).id, - }) - return result - @api.model def _timesheet_determine_sale_line(self, task, employee, project): """ Deduce the SO line associated to the timesheet line: From 4279de1689d198fcf9002592d7b567cb18ba64c7 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 7 Dec 2020 09:48:46 +0100 Subject: [PATCH 13/41] [ADD] sale_timesheet_edit: allow edition of so_line in timesheet This module allows the user to edit the so_line of each timesheets in a task in project.task form view. When the user manually edits the so_line of a timesheet, we need to store in is_so_line_edited that this field has been edited manually. We need to keep track this information because we need to keep the so_line selected by the user even if the configuration of the project changes or if the sale_line_id of the task changes. task-2409761 --- addons/sale_timesheet_edit/__init__.py | 4 ++ addons/sale_timesheet_edit/__manifest__.py | 25 +++++++++++ addons/sale_timesheet_edit/models/__init__.py | 5 +++ .../models/account_analytic_line.py | 20 +++++++++ addons/sale_timesheet_edit/models/project.py | 15 +++++++ .../static/src/js/so_line_one2many.js | 28 +++++++++++++ addons/sale_timesheet_edit/views/assets.xml | 10 +++++ .../views/project_task.xml | 41 +++++++++++++++++++ 8 files changed, 148 insertions(+) create mode 100644 addons/sale_timesheet_edit/__init__.py create mode 100644 addons/sale_timesheet_edit/__manifest__.py create mode 100644 addons/sale_timesheet_edit/models/__init__.py create mode 100644 addons/sale_timesheet_edit/models/account_analytic_line.py create mode 100644 addons/sale_timesheet_edit/models/project.py create mode 100644 addons/sale_timesheet_edit/static/src/js/so_line_one2many.js create mode 100644 addons/sale_timesheet_edit/views/assets.xml create mode 100644 addons/sale_timesheet_edit/views/project_task.xml diff --git a/addons/sale_timesheet_edit/__init__.py b/addons/sale_timesheet_edit/__init__.py new file mode 100644 index 0000000000000..dc5e6b693d19d --- /dev/null +++ b/addons/sale_timesheet_edit/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/sale_timesheet_edit/__manifest__.py b/addons/sale_timesheet_edit/__manifest__.py new file mode 100644 index 0000000000000..6178dbe013584 --- /dev/null +++ b/addons/sale_timesheet_edit/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +# TODO: [XBO] merge with sale_timesheet module in master +{ + 'name': 'Sales Timesheet Edit', + 'category': 'Hidden', + 'summary': 'Edit the sale order line linked in the timesheets', + 'description': """ +Allow to edit sale order line in the timesheets +=============================================== + +This module adds the edition of the sale order line +set in the timesheets. This allows adds more flexibility +to the user to easily change the sale order line on a +timesheet in task form view when it is needed. +""", + 'depends': ['sale_timesheet'], + 'data': [ + 'views/assets.xml', + 'views/project_task.xml', + ], + 'demo': [], + 'auto_install': True, +} diff --git a/addons/sale_timesheet_edit/models/__init__.py b/addons/sale_timesheet_edit/models/__init__.py new file mode 100644 index 0000000000000..bf8826f4bc040 --- /dev/null +++ b/addons/sale_timesheet_edit/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_analytic_line +from . import project diff --git a/addons/sale_timesheet_edit/models/account_analytic_line.py b/addons/sale_timesheet_edit/models/account_analytic_line.py new file mode 100644 index 0000000000000..69a9cc3df30bb --- /dev/null +++ b/addons/sale_timesheet_edit/models/account_analytic_line.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +# TODO: [XBO] merge with account.analytic.line in the sale_timesheet module in master. +class AccountAnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + is_so_line_edited = fields.Boolean() + + @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'employee_id') + def _compute_so_line(self): + super(AccountAnalyticLine, self.filtered(lambda t: not t.is_so_line_edited))._compute_so_line() + + def _check_sale_line_in_project_map(self): + # TODO: [XBO] remove me in master, now we authorize to manually edit the so_line, then this so_line can be different of the one in task/project/map_entry + # !!! Override of the method in sale_timesheet !!! + return diff --git a/addons/sale_timesheet_edit/models/project.py b/addons/sale_timesheet_edit/models/project.py new file mode 100644 index 0000000000000..b10bafddbcf8a --- /dev/null +++ b/addons/sale_timesheet_edit/models/project.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class Project(models.Model): + _inherit = 'project.project' + + def _get_not_billed_timesheets(self): + """ Get the timesheets not invoiced and the SOL has not manually been edited + FIXME: [XBO] this change must be done in the _update_timesheets_sale_line_id + rather than this method in master to keep the initial behaviour of this method. + """ + return super(Project, self)._get_not_billed_timesheets() - self.mapped('timesheet_ids').filtered('is_so_line_edited') diff --git a/addons/sale_timesheet_edit/static/src/js/so_line_one2many.js b/addons/sale_timesheet_edit/static/src/js/so_line_one2many.js new file mode 100644 index 0000000000000..bc638b04b3451 --- /dev/null +++ b/addons/sale_timesheet_edit/static/src/js/so_line_one2many.js @@ -0,0 +1,28 @@ +odoo.define('sale_timesheet_edit.so_line_many2one', function (require) { +"use strict"; + +const fieldRegistry = require('web.field_registry'); +const FieldOne2Many = require('web.relational_fields').FieldOne2Many; + +const SoLineOne2Many = FieldOne2Many.extend({ + _onFieldChanged: function (ev) { + if ( + ev.data.changes && + ev.data.changes.hasOwnProperty('timesheet_ids') && + ev.data.changes.timesheet_ids.operation === 'UPDATE' && + ev.data.changes.timesheet_ids.data.hasOwnProperty('so_line')) { + const line = this.value.data.find(line => { + return line.id === ev.data.changes.timesheet_ids.id; + }); + if (!line.is_so_line_edited) { + ev.data.changes.timesheet_ids.data.is_so_line_edited = true; + } + } + this._super.apply(this, arguments); + } +}); + + +fieldRegistry.add('so_line_one2many', SoLineOne2Many); + +}); diff --git a/addons/sale_timesheet_edit/views/assets.xml b/addons/sale_timesheet_edit/views/assets.xml new file mode 100644 index 0000000000000..b8909b4496452 --- /dev/null +++ b/addons/sale_timesheet_edit/views/assets.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/addons/sale_timesheet_edit/views/project_task.xml b/addons/sale_timesheet_edit/views/project_task.xml new file mode 100644 index 0000000000000..8ce54aa65931a --- /dev/null +++ b/addons/sale_timesheet_edit/views/project_task.xml @@ -0,0 +1,41 @@ + + + + + + project.task.form.view.form.inherit.sale.timesheet.edit + project.task + + + + so_line_one2many + + + 0 + [('is_service', '=', True), ('order_partner_id', 'child_of', parent.commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])] + {'no_create': True, 'no_open': True} + + + + + + + project.task.form.view.form.inherit.sale.timesheet.editable + project.task + + + + {'no_create': True} + + + + + + + + + From 9c01e06f06cff8e237392cfde7bb9670b5d5259e Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Mon, 7 Dec 2020 16:09:10 +0100 Subject: [PATCH 14/41] [FIX] project_sale: prevent invalid SOL on project Prior to this commit: - The user could set a SOL which product was not of 'service' type. After this commit: - The user will no more be able to set a SOL which product is not of 'service' type. task-2409761 --- addons/sale_project/models/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index a5050f2f80fe7..02eed4af3aa9f 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -12,7 +12,7 @@ class Project(models.Model): sale_line_id = fields.Many2one( 'sale.order.line', 'Sales Order Item', copy=False, - domain="[('is_expense', '=', False), ('order_id', '=', sale_order_id), ('state', 'in', ['sale', 'done']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", + domain="[('is_service', '=', True), ('is_expense', '=', False), ('order_id', '=', sale_order_id), ('state', 'in', ['sale', 'done']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. " "Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.") sale_order_id = fields.Many2one('sale.order', 'Sales Order', domain="[('partner_id', '=', partner_id)]", copy=False, help="Sales order to which the project is linked.") From e23b1f229cfff348d21abc3a579570ac6249a078 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 7 Dec 2020 17:07:14 +0100 Subject: [PATCH 15/41] [FIX] sale_timesheet: display so_line only if project is billable Before this commit, the so_line field of each timesheet is always deplayed in the task form even if the project is not billable. This commit adds a condition to only display this field when the project is billable. task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 5f7e9ee69200e..f26821d651b35 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -136,7 +136,7 @@ - + From 659418a695b9e8ae32ed1ab4aa88b9ebe2b9c665 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Tue, 8 Dec 2020 22:41:52 +0100 Subject: [PATCH 16/41] [IMP] sale_timesheet: move timesheet SOL before duration task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index f26821d651b35..c7673fc887c1f 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -134,9 +134,11 @@ - + + + From e18229b9a4335e2cfa9d01d11f9e99be2bd99f9c Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Wed, 9 Dec 2020 12:16:13 +0100 Subject: [PATCH 17/41] [IMP] project,sale_project: have partner_id many2One behave as in sale.order Prior to this commit the partners where sorted out by display_name in the task form view. After this commit the will be sorted out by their ranking, as it is the case in sale.order form view. task-2409761 --- addons/project/views/project_views.xml | 2 +- addons/sale_project/views/project_task_views.xml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 843418ff4c958..e2d8db2353506 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -670,7 +670,7 @@ - + diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index 436eeb0b02060..6cb6eb36b2814 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -26,6 +26,10 @@ string="Sales Order" groups="sales_team.group_sale_salesman"/> + + {'always_reload': True} + {'res_partner_search_mode': 'customer'} + From aba51f314324905d4d435c3283fa2361d84ad129 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Wed, 9 Dec 2020 13:42:11 +0100 Subject: [PATCH 18/41] [IMP] sale_timesheet: always hide SO field task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index c7673fc887c1f..ea7de9097aa53 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -169,6 +169,9 @@ + + 1 + From c4331560668f1eb56852a712e502ece78701d3fe Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Thu, 10 Dec 2020 08:01:38 +0100 Subject: [PATCH 19/41] [IMP] sale_project,sale_timesheet: set the last SOL of customer When the customer is set in the task form, we apply the compute, if the compute doesn't find a SOL, then we search the last SOL of this customer set and whose remaining hours > 0 to give this SOL to the current task. task-2409761 --- addons/sale_project/models/project.py | 2 +- addons/sale_timesheet/models/project.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 02eed4af3aa9f..b2895679d7567 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -48,7 +48,7 @@ def _compute_partner_id(self): task.partner_id = task.project_id.sale_line_id.order_partner_id super()._compute_partner_id() - @api.depends('partner_id.commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') + @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') def _compute_sale_line(self): for task in self: if not task.sale_line_id: diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index db984cfd22d04..597b858ded68a 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -294,6 +294,12 @@ def _compute_sale_order_id(self): elif not task.sale_order_id: task.sale_order_id = False + @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') + def _compute_sale_line(self): + super(ProjectTask, self)._compute_sale_line() + for task in self.filtered(lambda t: not t.sale_line_id): + task.sale_line_id = task._get_last_sol_of_customer() + @api.depends('project_id.sale_line_employee_ids') def _compute_is_project_map_empty(self): for task in self: @@ -352,6 +358,20 @@ def write(self, values): return res + def _get_last_sol_of_customer(self): + # Get the last SOL made for the customer in the current task where we need to compute + self.ensure_one() + if not self.commercial_partner_id or not self.allow_billable: + return False + domain = [('is_service', '=', True), ('order_partner_id', 'child_of', self.commercial_partner_id.id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])] + if self.project_id.bill_type == 'customer_type' and self.project_sale_order_id: + domain.append(('order_id', '=?', self.project_sale_order_id.id)) + sale_lines = self.env['sale.order.line'].search(domain) + for line in sale_lines: + if line.remaining_hours_available and line.remaining_hours > 0: + return line + return False + def action_make_billable(self): return { "name": _("Create Sales Order"), From daa1036eb6b54588224bc5d6fe9be6974cde1ea8 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Thu, 10 Dec 2020 16:32:32 +0100 Subject: [PATCH 20/41] [FIX] sale_timesheet: change condition to display SOL Since the SO in task is always invisible, we cannot set this field. Then the SOL will never display in the view because we need the SO will be set to display it. This commit changes the condition to remove the SO will be set to display the SOL. task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index ea7de9097aa53..dcc01ec6aabfe 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -152,7 +152,7 @@ {'with_remaining_hours': True} - {'invisible': ['|', '|', '|', ('allow_billable', '=', False), ('sale_order_id', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]} + {'invisible': ['|', '|', ('allow_billable', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]} From a41df84a83380dded4cb8c9d83444f916b9e004d Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Thu, 10 Dec 2020 18:34:47 +0100 Subject: [PATCH 21/41] [IMP] {hr,sale}_timesheet: review portal timesheets This commit improves the portal view containing the timesheets list (this view is available in the '/my/timesheets' route. In sale_timesheet module, we * add new group by and a search on SOL, * add an SOL field on the left of the unit_amount one, * change the group by by default, to group the timesheets by SOL * display the following information in the group: 'x Hours ordered, x Remaining', when the timesheets are grouped by SOL. However, the 'x Remaining' part should only be visible if the number of timesheets is greater than 0. * move the total number of hours in the group itself and align it to the right. * change the number of items displayed per page, 100 items instead of 20. * reduce the size of the font in tbody tag (0.8rem instead of 0.875rem) * display the content of the field on hover, it's usefull when the field it's cropped. * Add 'View Timesheets' button in portal SO: this new button button called 'View timesheets' is displayed next the name of the product of a SO in Sale Order Portal. it is only visible if the number of timehseets is greater than 0 for this SOL. The button redirects the user to /my/timesheets page with a filter on the corresponding SOL + a group by SOL. task-2409761 --- addons/hr_timesheet/controllers/portal.py | 79 ++++++++------ .../views/hr_timesheet_portal_templates.xml | 100 +++++++++++++----- addons/sale_timesheet/controllers/portal.py | 32 ++++++ .../views/sale_timesheet_portal_templates.xml | 37 +++++++ 4 files changed, 190 insertions(+), 58 deletions(-) diff --git a/addons/hr_timesheet/controllers/portal.py b/addons/hr_timesheet/controllers/portal.py index 8127b83be6f5e..b244ee3e65606 100644 --- a/addons/hr_timesheet/controllers/portal.py +++ b/addons/hr_timesheet/controllers/portal.py @@ -23,32 +23,59 @@ def _prepare_home_portal_values(self, counters): values['timesheet_count'] = request.env['account.analytic.line'].sudo().search_count(domain) return values + def _get_searchbar_inputs(self): + return { + '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')} + } + + def _get_searchbar_groupby(self): + return { + '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')} + } + + def _get_search_domain(self, search_in, 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)]]) + return search_domain + + def _get_groupby_mapping(self): + return { + 'project': 'project_id', + 'task': 'task_id', + 'employee': 'employee_id', + 'date': 'date' + } + @http.route(['/my/timesheets', '/my/timesheets/page/'], type='http', auth="user", website=True) 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() + _items_per_page = 100 searchbar_sortings = { 'date': {'label': _('Newest'), 'order': 'date desc'}, '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_inputs = self._get_searchbar_inputs() - 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')}, - } + searchbar_groupby = self._get_searchbar_groupby() today = fields.Date.today() quarter_start, quarter_end = date_utils.get_quarter(today) @@ -77,16 +104,7 @@ def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, domain = AND([domain, searchbar_filters[filterby]['domain']]) if search and search_in: - 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 + domain += self._get_search_domain(search_in, search) timesheet_count = Timesheet_sudo.search_count(domain) # pager @@ -95,19 +113,14 @@ def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, url_args={'sortby': sortby, 'search_in': search_in, 'search': search, 'filterby': filterby, 'groupby': groupby}, total=timesheet_count, page=page, - step=self._items_per_page + step=_items_per_page ) def get_timesheets(): - groupby_mapping = { - 'project': 'project_id', - 'task': 'task_id', - 'employee': 'employee_id', - 'date': 'date', - } + groupby_mapping = self._get_groupby_mapping() 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']) + timesheets = Timesheet_sudo.search(domain, order=orderby, limit=_items_per_page, offset=pager['offset']) if field: if groupby == 'date': time_data = Timesheet_sudo.read_group(domain, ['date', 'unit_amount:sum'], ['date:day']) @@ -115,7 +128,7 @@ def get_timesheets(): 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]) + mapped_time = dict([(m[field][0] if m[field] else False, 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 diff --git a/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml b/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml index 90f2d67992845..4385bbddd6a9a 100644 --- a/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml +++ b/addons/hr_timesheet/views/hr_timesheet_portal_templates.xml @@ -36,24 +36,74 @@ - + - - Timesheets for project: - - - - Timesheets for task: - - - - Timesheets on - - - - Timesheets for employee: - - + + + Timesheets for project: + + + + + Total: + + + Total: + + + + + + Timesheets for task: + + + + + Total: + + + Total: + + + + + + Timesheets on + + + + + Total: + + + Total: + + + + + + Timesheets for employee: + + + + + Total: + + + Total: + + + + + +
+ + Total: + + + Total: + +
Date @@ -61,21 +111,21 @@ Project Task Description - Days Spent (Total: ) - Hours Spent (Total: ) + Days Spent + Hours Spent - + - - - - + + + + - + diff --git a/addons/sale_timesheet/controllers/portal.py b/addons/sale_timesheet/controllers/portal.py index e4e04da487867..5b6003aeca5ab 100644 --- a/addons/sale_timesheet/controllers/portal.py +++ b/addons/sale_timesheet/controllers/portal.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import http, _ from odoo.http import request from odoo.osv import expression from odoo.addons.account.controllers import portal +from odoo.addons.hr_timesheet.controllers.portal import TimesheetCustomerPortal class PortalAccount(portal.PortalAccount): @@ -38,3 +40,33 @@ def _order_get_page_view_values(self, order, access_token, **kwargs): values['timesheets'] = request.env['account.analytic.line'].sudo().search(domain) values['is_uom_day'] = request.env['account.analytic.line'].sudo()._is_timesheet_encode_uom_day() return values + + +class SaleTimesheetCustomerPortal(TimesheetCustomerPortal): + + def _get_searchbar_inputs(self): + searchbar_inputs = super()._get_searchbar_inputs() + searchbar_inputs.update(sol={'input': 'sol', 'label': _('Search in Sales Order Item')}, sol_id={'input': 'sol_id', 'label': _('Search in Sales Order Item ID')}) + return searchbar_inputs + + def _get_searchbar_groupby(self): + searchbar_groupby = super()._get_searchbar_groupby() + searchbar_groupby.update(sol={'input': 'sol', 'label': _('Sales Order Item')}) + return searchbar_groupby + + def _get_search_domain(self, search_in, search): + search_domain = super()._get_search_domain(search_in, search) + if search_in in ('sol', 'all'): + search_domain = expression.OR([search_domain, [('so_line', 'ilike', search)]]) + if search_in == 'sol_id': + search_domain = expression.OR([search_domain, [('so_line.id', '=', search)]]) + return search_domain + + def _get_groupby_mapping(self): + groupby_mapping = super()._get_groupby_mapping() + groupby_mapping.update(sol='so_line') + return groupby_mapping + + @http.route(['/my/timesheets', '/my/timesheets/page/'], type='http', auth="user", website=True) + def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, search_in='all', groupby='sol', **kw): + return super().portal_my_timesheets(page, sortby, filterby, search, search_in, groupby, **kw) diff --git a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml index 9e198ababeef5..2f9ae255141b2 100644 --- a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml +++ b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml @@ -59,6 +59,12 @@
+ + + + From 782fbd8075d82393c1a33cc52f34006a9fb340a8 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Thu, 10 Dec 2020 19:52:25 +0100 Subject: [PATCH 22/41] [IMP] sale_timesheet: remove timesheets table in SO portal This view can be too long for the user and we don't give the total of the timesheets and also the SOL linked of these timesheets. This view is replaced by a button called 'View Timesheets' next the name of the product and this button redirects the user to the /my/timesheets page with a filter on the corresponding SOL. task-2409761 --- .../views/sale_timesheet_portal_templates.xml | 89 ++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml index 2f9ae255141b2..e26913ecec213 100644 --- a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml +++ b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml @@ -31,7 +31,7 @@ - + @@ -65,55 +65,60 @@ + From cfb5eb052a4400053d18bc7494b16b2d23ef5ae6 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Fri, 11 Dec 2020 11:53:48 +0100 Subject: [PATCH 23/41] [FIX] sale_timesheet: change uom in a demo product Before this commit, the uom of 'Service on Timesheet' product is in Unit (it's the uom by default). In this commit, we change the uom of this product to set it as product_uom_hour task-2409761 --- addons/sale_timesheet/data/sale_service_data.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/sale_timesheet/data/sale_service_data.xml b/addons/sale_timesheet/data/sale_service_data.xml index b0042b6acdcec..1cc43e67f4c8b 100644 --- a/addons/sale_timesheet/data/sale_service_data.xml +++ b/addons/sale_timesheet/data/sale_service_data.xml @@ -5,6 +5,8 @@ Service on Timesheet service 40 + + delivered_timesheet From 63fdab860d69decf3e383ae73e58163581f77a6c Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Fri, 11 Dec 2020 13:32:37 +0100 Subject: [PATCH 24/41] [IMP] sale_timesheet: activate group_uom For tasks that are linked to a SOL with a product which both have a service_policy set to 'ordered_timesheet and an unit of measure which is of "time" category , the sale_timesheet app adds the possibility to see (from the Task Form) the number of hours that can still be timesheeted in comparision with what has been sold in the related SO. This is why this commit add the fact that the setting uom_group is set to True. task-2409761 --- addons/sale_timesheet/data/sale_service_data.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/addons/sale_timesheet/data/sale_service_data.xml b/addons/sale_timesheet/data/sale_service_data.xml index 1cc43e67f4c8b..dbfcea047ccbe 100644 --- a/addons/sale_timesheet/data/sale_service_data.xml +++ b/addons/sale_timesheet/data/sale_service_data.xml @@ -11,4 +11,9 @@ + + + + + From 563c6a127c83f19ef0caa33eb392595b14924d39 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Mon, 14 Dec 2020 10:40:30 +0100 Subject: [PATCH 25/41] [IMP] project: hide partner phone number task-2409761 --- addons/project/views/project_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index e2d8db2353506..3613446676f0f 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -672,7 +672,7 @@ - + From efba0148a843e35cdcdfe3228f718d8fe8634f56 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 14 Dec 2020 11:19:57 +0100 Subject: [PATCH 26/41] [FIX] sale_timesheet: determine SOL only if allow_billable=True Before this commit, even if the allow_billable=False in the current project, we search a SOL for task and timesheets. This commit determines the SOL in tasks and timesheets only if the allow_billable=True in the linked project. task-2409761 --- addons/sale_timesheet/models/account.py | 6 ++--- addons/sale_timesheet/models/project.py | 23 ++++--------------- .../models/account_analytic_line.py | 2 +- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 5d2c503e9b1df..74b039fa6d3e8 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -57,10 +57,10 @@ def _onchange_task_id_employee_id(self): else: self.so_line = False - @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'employee_id') + @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'employee_id', 'project_id.allow_billable') def _compute_so_line(self): for timesheet in self._get_not_billed(): # Get only the timesheets are not yet invoiced - timesheet.so_line = timesheet._timesheet_determine_sale_line(timesheet.task_id, timesheet.employee_id, timesheet.project_id) + timesheet.so_line = timesheet.project_id.allow_billable and timesheet._timesheet_determine_sale_line(timesheet.task_id, timesheet.employee_id, timesheet.project_id) def _get_not_billed(self): return self.filtered(lambda t: not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') @@ -94,8 +94,6 @@ def _timesheet_preprocess(self, values): values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) # task implies so line (at create) if any([field_name in values for field_name in ['task_id', 'project_id']]) and not values.get('so_line') and (values.get('employee_id') or self.mapped('employee_id')): - if not values.get('employee_id') and len(self.mapped('employee_id')) > 1: - raise UserError(_('You can not modify timesheets from different employees')) task = self.env['project.task'].sudo().browse(values['task_id']) if values.get('task_id') else self.env['project.task'] employee = self.env['hr.employee'].sudo().browse(values['employee_id']) if values.get('employee_id') else self.mapped('employee_id') project = self.env['project.project'].sudo().browse(values['project_id']) if values.get('project_id') else task.project_id diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 597b858ded68a..cf215b5dd0923 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -294,10 +294,11 @@ def _compute_sale_order_id(self): elif not task.sale_order_id: task.sale_order_id = False - @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') + @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable') def _compute_sale_line(self): - super(ProjectTask, self)._compute_sale_line() - for task in self.filtered(lambda t: not t.sale_line_id): + billable_tasks = self.filtered('allow_billable') + super(ProjectTask, billable_tasks)._compute_sale_line() + for task in billable_tasks.filtered(lambda t: not t.sale_line_id): task.sale_line_id = task._get_last_sol_of_customer() @api.depends('project_id.sale_line_employee_ids') @@ -325,22 +326,6 @@ def write(self, values): project_dest = self.env['project.project'].browse(values['project_id']) if project_dest.bill_type == 'customer_project' and project_dest.pricing_type == 'employee_rate': self.write({'sale_line_id': False}) - if 'sale_line_id' in values and self.filtered('allow_timesheets').sudo().timesheet_ids: - so = self.env['sale.order.line'].browse(values['sale_line_id']).order_id - if so and not so.analytic_account_id: - so.analytic_account_id = self.project_id.analytic_account_id - timesheet_ids = self.filtered('allow_timesheets').timesheet_ids.filtered( - lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') - ) - timesheet_ids.write({'so_line': values['sale_line_id']}) - if 'project_id' in values: - - # Special case when we edit SOL an project in same time, as we edit SOL of - # timesheet lines, function '_get_timesheet' won't find the right timesheet - # to edit so we must edit those here. - project = self.env['project.project'].browse(values.get('project_id')) - if project.allow_timesheets: - timesheet_ids.write({'project_id': values.get('project_id')}) if 'non_allow_billable' in values and self.filtered('allow_timesheets').sudo().timesheet_ids: timesheet_ids = self.filtered('allow_timesheets').timesheet_ids.filtered( lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') diff --git a/addons/sale_timesheet_edit/models/account_analytic_line.py b/addons/sale_timesheet_edit/models/account_analytic_line.py index 69a9cc3df30bb..5ff797ddc34d7 100644 --- a/addons/sale_timesheet_edit/models/account_analytic_line.py +++ b/addons/sale_timesheet_edit/models/account_analytic_line.py @@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model): is_so_line_edited = fields.Boolean() - @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'employee_id') + @api.depends('task_id.sale_line_id', 'project_id.sale_line_id', 'project_id.allow_billable', 'employee_id') def _compute_so_line(self): super(AccountAnalyticLine, self.filtered(lambda t: not t.is_so_line_edited))._compute_so_line() From a3c3487d423d9fbff666f611cdf15ceeffa2e6f6 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 14 Dec 2020 14:15:33 +0100 Subject: [PATCH 27/41] [IMP] sale_timesheet: set allow_billable to True In this commit, we set the allow_billable to True in the After-Sales Service project. This project is a demo data. task-2409761 --- addons/sale_timesheet/data/sale_service_demo.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/sale_timesheet/data/sale_service_demo.xml b/addons/sale_timesheet/data/sale_service_demo.xml index 0b64fa6533704..3f1832b8a84e8 100644 --- a/addons/sale_timesheet/data/sale_service_demo.xml +++ b/addons/sale_timesheet/data/sale_service_demo.xml @@ -22,6 +22,7 @@ After-Sales Services + From 48db479b47b8ec6ed87faad1296b411298ae114b Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 14 Dec 2020 18:34:35 +0100 Subject: [PATCH 28/41] [FIX] sale_project: display SOL in task for all --- addons/sale_project/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index 6cb6eb36b2814..cecf404c29773 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -32,7 +32,7 @@ - + From 493efbbeaebe2776b719b0ff100f44e13667233d Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 14 Dec 2020 19:13:24 +0100 Subject: [PATCH 29/41] [FIX] sale_timesheet: remaining hours and SOL Before this commit, the remaining_hours_on_so and SOL on task are not displayed if the project employee rate. This commit removes this condition to display these when the project is employee rate. task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index dcc01ec6aabfe..d0ed1b06af94a 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -152,12 +152,12 @@ {'with_remaining_hours': True} - {'invisible': ['|', '|', ('allow_billable', '=', False), '&', ('bill_type', '=', 'customer_project'), ('pricing_type', '=', 'employee_rate'), ('partner_id', '=', False)]} + {'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]} - + 0 - [('is_service', '=', True), ('order_partner_id', 'child_of', parent.commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])] + [('is_service', '=', True), ('order_partner_id', 'child_of', parent.commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_id', '=?', parent.project_sale_order_id)] {'no_create': True, 'no_open': True} From 393769b1445bd5d310cd434144a770094b430f00 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Tue, 15 Dec 2020 09:13:24 +0100 Subject: [PATCH 31/41] [IMP] sale_project: hide SO Before this commit, the SO was hidden in the sale_timesheet app and not in sale_project. After this commit the SO field will be hidden as soon as sale_project is installed. task-2409761 --- addons/sale_project/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_project/views/project_task_views.xml b/addons/sale_project/views/project_task_views.xml index cecf404c29773..69049599c0b1d 100644 --- a/addons/sale_project/views/project_task_views.xml +++ b/addons/sale_project/views/project_task_views.xml @@ -31,7 +31,7 @@ {'res_partner_search_mode': 'customer'} - + From c247ce57e62181ca50930d76ba2134220d3dce42 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Tue, 15 Dec 2020 10:46:40 +0100 Subject: [PATCH 32/41] [IMP] project,sale_project: have partner_id many2One behave as in sale.order Prior to this commit the partners where sorted out by display_name in the project form view. After this commit the will be sorted out by their ranking, as it is the case in sale.order form view. task-2409761 --- addons/project/views/project_views.xml | 2 +- addons/sale_project/views/project_task_views.xml | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 3613446676f0f..0b8f1e2df6da4 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -294,7 +294,7 @@ - + + + project.project.view.inherit + project.project + + + + {'always_reload': True} + {'res_partner_search_mode': 'customer'} + + + + project.task.view.inherit project.task From 83c0f908d1e5c025311f264d103d868149485bf1 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 15 Dec 2020 14:24:11 +0100 Subject: [PATCH 33/41] [FIX] sale_project: Display SO with services only Purpose ======= On the project form view, only propose SO with the related customer + SO on which there is at least one line with a service product. --- addons/sale_project/models/project.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index b2895679d7567..adcddc144c654 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -15,7 +15,9 @@ class Project(models.Model): domain="[('is_service', '=', True), ('is_expense', '=', False), ('order_id', '=', sale_order_id), ('state', 'in', ['sale', 'done']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. " "Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.") - sale_order_id = fields.Many2one('sale.order', 'Sales Order', domain="[('partner_id', '=', partner_id)]", copy=False, help="Sales order to which the project is linked.") + sale_order_id = fields.Many2one('sale.order', 'Sales Order', + domain="[('order_line.product_id.type', '=', 'service'), ('partner_id', '=', partner_id)]", + copy=False, help="Sales order to which the project is linked.") _sql_constraints = [ ('sale_order_required_if_sale_line', "CHECK((sale_line_id IS NOT NULL AND sale_order_id IS NOT NULL) OR (sale_line_id IS NULL))", 'The project should be linked to a sale order to select a sale order item.'), From 8e86c92ef7b3d7252e4830ba39952690505e8292 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Tue, 15 Dec 2020 14:31:52 +0100 Subject: [PATCH 34/41] [FIX] sale_timesheet: apply same visibility to remaining_hours_so and its label Prior to this commit the visibiility rules of remaining_hours_so and its label were different. After this commit the visibility rule of remaining_hours_so and its label are the same. task-2409761 --- addons/sale_timesheet/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index d0ed1b06af94a..e134553abd205 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -167,7 +167,7 @@ 1 From 62eb13d2469e246b04bcbb662b8536a080b77a80 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 15 Dec 2020 14:49:12 +0100 Subject: [PATCH 35/41] [FIX] sale_timesheet: Make SO/SOL no_open for non salesman Purpose ======= People should be able to see it, but not to open it, as it would result into a access error (Obviously as the user hasn't the right to read a SO/SOL. --- .../views/project_task_views.xml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index e134553abd205..ac4b360a36d4f 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -25,8 +25,8 @@ - - + + @@ -60,6 +60,21 @@ + + project.project.form.inherit.salesman + project.project + + + + + {'no_create': True, 'no_edit': True, 'delete': False} + + + {'no_create': True, 'no_edit': True, 'delete': False} + + + + project.project.view.form.simplified.inherit project.project From 7c2a9ef18ff2b808377a05618c8b734c9f835c64 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 15 Dec 2020 14:58:47 +0100 Subject: [PATCH 36/41] [FIX] sale_timesheet: Fix view dependency issue --- addons/sale_timesheet/views/project_task_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index ac4b360a36d4f..1fcc110d8e0a6 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -162,7 +162,7 @@ view.task.form2.inherit project.task - + {'with_remaining_hours': True} From 509a58025d1c61cefc7305f920acb197915e5485 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 15 Dec 2020 14:59:51 +0100 Subject: [PATCH 37/41] [IMP] sale_timesheet: remove timesheets table in invoice portal This view can be too long for the user and we don't give the total of the timesheets and also the SOL linked of these timesheets. This view is replaced by a button called 'View Timesheets' next the name of the product and this button redirects the user to the /my/timesheets page with a filter on the corresponding product in the invoice. task-2409761 --- addons/sale_timesheet/__manifest__.py | 1 + addons/sale_timesheet/controllers/portal.py | 13 ++- .../sale_timesheet/views/report_invoice.xml | 10 +++ .../views/sale_timesheet_portal_templates.xml | 87 ++++++++++--------- 4 files changed, 69 insertions(+), 42 deletions(-) create mode 100644 addons/sale_timesheet/views/report_invoice.xml diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py index eb640411bdb07..577e5cba62c2e 100644 --- a/addons/sale_timesheet/__manifest__.py +++ b/addons/sale_timesheet/__manifest__.py @@ -25,6 +25,7 @@ 'views/hr_timesheet_views.xml', 'views/res_config_settings_views.xml', 'views/hr_timesheet_templates.xml', + 'views/report_invoice.xml', 'views/sale_timesheet_portal_templates.xml', 'report/project_profitability_report_analysis_views.xml', 'data/sale_timesheet_filters.xml', diff --git a/addons/sale_timesheet/controllers/portal.py b/addons/sale_timesheet/controllers/portal.py index 5b6003aeca5ab..7d13914dc79d9 100644 --- a/addons/sale_timesheet/controllers/portal.py +++ b/addons/sale_timesheet/controllers/portal.py @@ -46,7 +46,10 @@ class SaleTimesheetCustomerPortal(TimesheetCustomerPortal): def _get_searchbar_inputs(self): searchbar_inputs = super()._get_searchbar_inputs() - searchbar_inputs.update(sol={'input': 'sol', 'label': _('Search in Sales Order Item')}, sol_id={'input': 'sol_id', 'label': _('Search in Sales Order Item ID')}) + searchbar_inputs.update( + sol={'input': 'sol', 'label': _('Search in Sales Order Item')}, + sol_id={'input': 'sol_id', 'label': _('Search in Sales Order Item ID')}, + invoice={'input': 'invoice_id', 'label': _('Search in Invoice ID')}) return searchbar_inputs def _get_searchbar_groupby(self): @@ -58,8 +61,14 @@ def _get_search_domain(self, search_in, search): search_domain = super()._get_search_domain(search_in, search) if search_in in ('sol', 'all'): search_domain = expression.OR([search_domain, [('so_line', 'ilike', search)]]) + if search_in in ('sol_id', 'invoice_id'): + search = int(search) if search.isdigit() else 0 if search_in == 'sol_id': search_domain = expression.OR([search_domain, [('so_line.id', '=', search)]]) + if search_in == 'invoice_id': + invoice = request.env['account.move'].browse(search) + domain = request.env['account.analytic.line']._timesheet_get_sale_domain(invoice.mapped('invoice_line_ids.sale_line_ids'), invoice) + search_domain = expression.OR([search_domain, domain]) return search_domain def _get_groupby_mapping(self): @@ -69,4 +78,6 @@ def _get_groupby_mapping(self): @http.route(['/my/timesheets', '/my/timesheets/page/'], type='http', auth="user", website=True) def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, search_in='all', groupby='sol', **kw): + if search and search_in and search_in in ('sol_id', 'invoice_id') and not search.isdigit(): + search = '0' return super().portal_my_timesheets(page, sortby, filterby, search, search_in, groupby, **kw) diff --git a/addons/sale_timesheet/views/report_invoice.xml b/addons/sale_timesheet/views/report_invoice.xml new file mode 100644 index 0000000000000..dbf89fc24077a --- /dev/null +++ b/addons/sale_timesheet/views/report_invoice.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml index e26913ecec213..d983b28c09590 100644 --- a/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml +++ b/addons/sale_timesheet/views/sale_timesheet_portal_templates.xml @@ -7,55 +7,60 @@ + From d64dfb7d4309db1ad333d79d96e9bac889d4c83c Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Wed, 16 Dec 2020 14:42:57 +0100 Subject: [PATCH 38/41] [FIX] sale_timesheet: remove the preprocess on SOL When the user want to Add a timesheet in the task/ticket, removes the SOL assigned to the timesheets by the _compute_so_line and finally save the changes in the task. Then, the so_line is not removed, because the _timesheet_preprocess method determines a SOL on new timesheet if there is no SOL on it. This commit removes the processing in the _timesheet_preprocess method to keep the correct expected behaviour. task-2409761 --- addons/sale_timesheet/models/account.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 74b039fa6d3e8..ef89280ab97df 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -92,12 +92,6 @@ def _timesheet_preprocess(self, values): values['account_id'] = task.analytic_account_id.id values['company_id'] = task.analytic_account_id.company_id.id values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) - # task implies so line (at create) - if any([field_name in values for field_name in ['task_id', 'project_id']]) and not values.get('so_line') and (values.get('employee_id') or self.mapped('employee_id')): - task = self.env['project.task'].sudo().browse(values['task_id']) if values.get('task_id') else self.env['project.task'] - employee = self.env['hr.employee'].sudo().browse(values['employee_id']) if values.get('employee_id') else self.mapped('employee_id') - project = self.env['project.project'].sudo().browse(values['project_id']) if values.get('project_id') else task.project_id - values['so_line'] = self._timesheet_determine_sale_line(task, employee, project).id return values @api.model From 69c7d96e1671266e939163bde05afe684f907606 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Wed, 16 Dec 2020 14:49:27 +0100 Subject: [PATCH 39/41] [IMP] sale_timesheet: change SO compute behaviour Before this commit, the compute doesn't assigned an SO on a task when the project linked has not SO and the pricing type is not task rate. Because, we give only the SO of the project to the task when the project is employee rate or project rate. This commit changes the implementation of the _compute_sale_order_id in task. Now, if the allow_billable=False or non_allow_billable=True then the SO is False. Otherwise, we give the SO of the SOL selected in the task or the SO in the project. And moreover, if the partner_id is not set, we give the partner_id defined in the SO assigned. task-2409761 --- addons/sale_timesheet/models/project.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index cf215b5dd0923..35d4751731ed3 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -282,17 +282,18 @@ def _compute_analytic_account_active(self): for task in self: task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active - @api.depends('sale_line_id', 'project_id', 'allow_billable', 'bill_type', 'pricing_type', 'non_allow_billable') + @api.depends('sale_line_id', 'project_id', 'allow_billable', 'non_allow_billable') def _compute_sale_order_id(self): for task in self: - if task.allow_billable and task.bill_type == 'customer_project' and task.pricing_type == 'employee_rate' and task.non_allow_billable: - task.sale_order_id = False - elif task.allow_billable and task.bill_type == 'customer_project': - task.sale_order_id = task.project_id.sale_order_id - elif task.allow_billable and task.bill_type == 'customer_task': - task.sale_order_id = task.sale_line_id.sudo().order_id - elif not task.sale_order_id: + if not task.allow_billable or task.non_allow_billable: task.sale_order_id = False + elif task.allow_billable: + if task.sale_line_id: + task.sale_order_id = task.sale_line_id.sudo().order_id + elif task.project_id.sale_order_id: + task.sale_order_id = task.project_id.sale_order_id + if task.sale_order_id and not task.partner_id: + task.partner_id = task.sale_order_id.partner_id @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable') def _compute_sale_line(self): From 3f3d2b257c7f0ee156bf7439ea807f422970a278 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Fri, 18 Dec 2020 16:34:10 +0100 Subject: [PATCH 40/41] [FIX] sale_timesheet: Only open SO for salesman on project overview Purpose ======= People should be able to see it, but not to open it, as it would result into a access error (Obviously as the user hasn't the right to read a SO/SOL). task-2409761 --- addons/sale_timesheet/views/hr_timesheet_templates.xml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index 6a44f41789b9f..1715f3c27354e 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -404,9 +404,14 @@ - + + + + + + - + Cancelled From 171260006af954245cde3f53b6eaa2d230316529 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Fri, 18 Dec 2020 17:06:12 +0100 Subject: [PATCH 41/41] [IMP] portal: increase search input width Since the new filter in sale_timesheet, the search box size was no wide any more to handle the placeholder text. task-2409761 --- addons/portal/views/portal_templates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 2d2e496ad0343..371391c24daa3 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -296,7 +296,7 @@ -
+
DateDate Employee Project Task