From 5236dff955c4e0dd49c1d964c96c7e24eac0aa31 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Thu, 4 Feb 2021 16:46:15 +0100 Subject: [PATCH 1/7] [MOV] sale_timesheet: move the timesheet_product_id field in mappings Before this commit, the timesheet_product_id field in project.sale.line.employee.map model is defined but never used. This commit moves the definition of this field in the industry_fsm_sale module to use this field in that module. task-2376382 closes odoo/odoo#66157 --- .../models/project_sale_line_employee_map.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/addons/sale_timesheet/models/project_sale_line_employee_map.py b/addons/sale_timesheet/models/project_sale_line_employee_map.py index d1b5e2322fa..c5bac16b553 100644 --- a/addons/sale_timesheet/models/project_sale_line_employee_map.py +++ b/addons/sale_timesheet/models/project_sale_line_employee_map.py @@ -12,13 +12,6 @@ class ProjectProductEmployeeMap(models.Model): employee_id = fields.Many2one('hr.employee', "Employee", required=True) sale_line_id = fields.Many2one('sale.order.line', "Sale Order Item", compute="_compute_sale_line_id", store=True, readonly=False, required=True, domain=[('is_service', '=', True)]) company_id = fields.Many2one('res.company', string='Company', related='project_id.company_id') - timesheet_product_id = fields.Many2one( - 'product.product', string='Service', - domain="""[ - ('type', '=', 'service'), - ('invoice_policy', '=', 'delivery'), - ('service_type', '=', 'timesheet'), - '|', ('company_id', '=', False), ('company_id', '=', company_id)]""") price_unit = fields.Float("Unit Price", compute='_compute_price_unit', store=True, readonly=True) currency_id = fields.Many2one('res.currency', string="Currency", compute='_compute_price_unit', store=True, readonly=False) @@ -30,26 +23,16 @@ class ProjectProductEmployeeMap(models.Model): def _compute_sale_line_id(self): self.filtered(lambda map_entry: not map_entry.project_id.sale_order_id and map_entry.sale_line_id).update({'sale_line_id': None}) - @api.depends('sale_line_id', 'sale_line_id.price_unit', 'timesheet_product_id') + @api.depends('sale_line_id', 'sale_line_id.price_unit') def _compute_price_unit(self): for line in self: if line.sale_line_id: line.price_unit = line.sale_line_id.price_unit line.currency_id = line.sale_line_id.currency_id - elif line.timesheet_product_id: - line.price_unit = line.timesheet_product_id.lst_price - line.currency_id = line.timesheet_product_id.currency_id else: line.price_unit = 0 line.currency_id = False - @api.onchange('timesheet_product_id') - def _onchange_timesheet_product_id(self): - if self.timesheet_product_id: - self.price_unit = self.timesheet_product_id.lst_price - else: - self.price_unit = 0.0 - @api.model def create(self, values): res = super(ProjectProductEmployeeMap, self).create(values) From 14e36685c12539b194609d5248f44d2d2ef4e115 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Fri, 5 Feb 2021 17:35:02 +0100 Subject: [PATCH 2/7] [IMP] sale_timesheet: render _timesheet_determine_sale_line no static Before this commit, this method is static to the model. But in fact, we always use the project, employee and the task from the current timesheet. So it can be better to directly have this information in the self. This commit changes this method to have no params into it and only use the self to retrieve the useful data. task-2376382 closes odoo/odoo#66157 --- addons/sale_timesheet/models/account.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 852384d140d..f3749791256 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -46,7 +46,7 @@ class AccountAnalyticLine(models.Model): @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.filtered(lambda t: not t.is_so_line_edited and t._is_not_billed()): # Get only the timesheets are not yet invoiced - timesheet.so_line = timesheet.project_id.allow_billable and 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() def _is_not_billed(self): self.ensure_one() @@ -76,29 +76,29 @@ class AccountAnalyticLine(models.Model): values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) return values - @api.model - def _timesheet_determine_sale_line(self, task, employee, project): + def _timesheet_determine_sale_line(self): """ Deduce the SO line associated to the timesheet line: 1/ timesheet on task rate: the so line will be the one from the task 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 not task: - if project.pricing_type == 'employee_rate': - map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', project.id), ('employee_id', '=', employee.id)]) + self.ensure_one() + + if not self.task_id: + if self.project_id.pricing_type == 'employee_rate': + map_entry = self.env['project.sale.line.employee.map'].search([('project_id', '=', self.project_id.id), ('employee_id', '=', self.employee_id.id)]) if map_entry: return map_entry.sale_line_id - if project.sale_line_id: - return project.sale_line_id - if task.allow_billable and task.sale_line_id: - if task.pricing_type in ('task_rate', 'fixed_rate'): - return task.sale_line_id + if self.project_id.sale_line_id: + return self.project_id.sale_line_id + if self.task_id.allow_billable and self.task_id.sale_line_id: + if self.task_id.pricing_type in ('task_rate', 'fixed_rate'): + return self.task_id.sale_line_id else: # then pricing_type = 'employee_rate' - map_entry = project.sale_line_employee_ids.filtered(lambda map_entry: map_entry.employee_id == employee) + map_entry = self.project_id.sale_line_employee_ids.filtered(lambda map_entry: map_entry.employee_id == self.employee_id) if map_entry: return map_entry.sale_line_id - if task.sale_line_id or project.sale_line_id: - return task.sale_line_id or project.sale_line_id + return self.task_id.sale_line_id return False def _timesheet_get_portal_domain(self): From 00ce1211f9764b32b6a9ed22c7ac299e0ec21dfa Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Fri, 12 Feb 2021 11:14:07 +0100 Subject: [PATCH 3/7] [FIX] sale_timesheet: change project for non billed timesheets Before this commit, when the user changes the project_id of a task, all timesheets from this task have not the new project, because only the timesheets not invoiced and the ones with no SOL or with the same SOL than the task are updated. The problem is the not invoiced timesheets with another SOL than the one in the task keep the old project. This commit changes the condition to change the project for the timesheets are not invoiced. task-2376382 closes odoo/odoo#66157 --- addons/sale_timesheet/models/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index d7bb125d332..5dbe7f111bf 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -284,7 +284,7 @@ class ProjectTask(models.Model): def _get_timesheet(self): # return not invoiced timesheet and timesheet without so_line or so_line linked to task timesheet_ids = super(ProjectTask, self)._get_timesheet() - return timesheet_ids.filtered(lambda t: (not t.timesheet_invoice_id or t.timesheet_invoice_id.state == 'cancel') and (not t.so_line or t.so_line == t.task_id._origin.sale_line_id)) + return timesheet_ids.filtered(lambda t: t._is_not_billed()) def _get_action_view_so_ids(self): return list(set((self.sale_order_id + self.timesheet_ids.so_line.order_id).ids)) From ab9eea075947edafd616e5c26abb5d82ba218448 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Mon, 15 Feb 2021 10:41:52 +0100 Subject: [PATCH 4/7] [IMP] sale_timesheet: let the SOL when project changed to employee rate Before this commit, when the user changes the project on the task to choose a project with the pricing type to employee rate, then the SOL on this task is removed. This commit removes this behaviour to let the SOL of the task. task-2376382 closes odoo/odoo#66157 --- addons/sale_timesheet/models/project.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index 5dbe7f111bf..01268b00d40 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -262,15 +262,6 @@ class ProjectTask(models.Model): if not self.sale_line_id: self.sale_line_id = self.project_id.sale_line_id - def write(self, values): - res = super(ProjectTask, self).write(values) - # Done after super to avoid constraints on field recomputation - if values.get('project_id'): - project_dest = self.env['project.project'].browse(values['project_id']) - if project_dest.pricing_type == 'employee_rate': - self.write({'sale_line_id': False}) - 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() From 1b60102b20bb27acebdc6161d1ab264d59e011c8 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 2 Mar 2021 17:24:55 +0100 Subject: [PATCH 5/7] [IMP] sale_timesheet: rename label of timesheet_product_id in project Before this commit, the label for timesheet_product_id field in the project form view was "Service". Now with this commit, this label is equal to "Default Service". task-2376382 closes #66157 --- 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 c11c5e547b7..1fd2d48ca61 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -23,7 +23,7 @@ - + From ffae2fd62697aa7104e2049bad4b3add9649d04f Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Thu, 4 Mar 2021 15:17:27 +0100 Subject: [PATCH 6/7] [IMP] sale_timesheet: display price_unit in SOL name_get Before this commit, when many sales order items have a same product with a different price_unit, the user cannot know which one of these sales order items has the product with the expensive price with just the name_get of the SOL in task form view. This commit adds the price_unit in the name_get of the sale order items only for the sales order items in a same sales order have the same product. task-2376382 closes #66157 --- addons/sale_timesheet/models/sale_order.py | 85 ++++++++++++------- .../views/project_task_views.xml | 4 +- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 5d16f85201a..c2d5ed1a8ea 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -2,10 +2,11 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import math +from collections import defaultdict from odoo import api, fields, models, _ from odoo.osv import expression -from odoo.tools import float_compare +from odoo.tools import float_compare, format_amount class SaleOrder(models.Model): @@ -134,41 +135,61 @@ class SaleOrderLine(models.Model): def name_get(self): res = super(SaleOrderLine, self).name_get() - if self.env.context.get('with_remaining_hours'): + with_remaining_hours = self.env.context.get('with_remaining_hours') + with_price_unit = self.env.context.get('with_price_unit') + if with_remaining_hours or with_price_unit: names = dict(res) result = [] - uom_hour = self.env.ref('uom.product_uom_hour') - uom_day = self.env.ref('uom.product_uom_day') + uom_hour = with_remaining_hours and self.env.ref('uom.product_uom_hour') + uom_day = with_remaining_hours and self.env.ref('uom.product_uom_day') + sols_by_so_dict = with_price_unit and defaultdict(lambda: self.env[self._name]) # key: (sale_order_id, product_id), value: sale order line 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') + if with_remaining_hours: + 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 ) - name = '{name}{remaining_time}'.format( - name=name, - remaining_time=remaining_time - ) - result.append((line.id, name)) + if with_price_unit: + names[line.id] = name + if not with_price_unit: + result.append((line.id, name)) + if with_price_unit: + sols_by_so_dict[line.order_id.id, line.product_id.id] += line + + if with_price_unit: + for sols in sols_by_so_dict.values(): + if len(sols) > 1: + result += [( + line.id, + '%s - %s' % ( + names.get(line.id), format_amount(self.env, line.price_unit, line.currency_id)) + ) for line in sols] + else: + result.append((sols.id, names.get(sols.id))) return result return res diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 1fd2d48ca61..bde6ee3ff92 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -153,7 +153,7 @@ @@ -192,7 +192,7 @@ - {'with_remaining_hours': True} + {'with_remaining_hours': True, 'with_price_unit': True} {'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]} From 25cbc6a7e3f067e5be86ddff186126d91c794e91 Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Tue, 9 Mar 2021 11:36:20 +0100 Subject: [PATCH 7/7] [IMP] sale_timesheet: edit bottom the employee mapping Before this commit, when the user adds a line in the employee mappings from the project form view, the new line appears at the top of the list. This commit changes to display this new line at the bottom of the list. task-2376382 closes #66157 --- 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 bde6ee3ff92..d74694ec902 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -29,7 +29,7 @@ - +