diff --git a/addons/sale_project/models/__init__.py b/addons/sale_project/models/__init__.py index 27c511ccd68..4c1f1f6332d 100644 --- a/addons/sale_project/models/__init__.py +++ b/addons/sale_project/models/__init__.py @@ -3,3 +3,4 @@ from . import product from . import project from . import sale_order +from . import sale_order_line diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index 7df1c457e88..ad9346916a7 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, Command, fields, models, _ - +from odoo import api, fields, models, _ from odoo.tools.safe_eval import safe_eval -from odoo.tools.sql import column_exists, create_column class SaleOrder(models.Model): @@ -114,283 +112,3 @@ class SaleOrder(models.Model): if 'state' in values and values['state'] == 'cancel': self.project_id.sale_line_id = False return super(SaleOrder, self).write(values) - - -class SaleOrderLine(models.Model): - _inherit = "sale.order.line" - - project_id = fields.Many2one( - 'project.project', 'Generated Project', - index=True, copy=False, help="Project generated by the sales order item") - task_id = fields.Many2one( - 'project.task', 'Generated Task', - index=True, copy=False, help="Task generated by the sales order item") - is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, compute_sudo=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.") - - @api.depends('product_id') - def _compute_is_service(self): - for so_line in self: - so_line.is_service = so_line.product_id.type == 'service' - - @api.depends('product_id') - def _compute_product_updatable(self): - for line in self: - if line.product_id.type == 'service' and line.state == 'sale': - line.product_updatable = False - else: - super(SaleOrderLine, line)._compute_product_updatable() - - def _auto_init(self): - """ - Create column to stop ORM from computing it himself (too slow) - """ - if not column_exists(self.env.cr, 'sale_order_line', 'is_service'): - create_column(self.env.cr, 'sale_order_line', 'is_service', 'bool') - self.env.cr.execute(""" - UPDATE sale_order_line line - SET is_service = (pt.type = 'service') - FROM product_product pp - LEFT JOIN product_template pt ON pt.id = pp.product_tmpl_id - WHERE pp.id = line.product_id - """) - return super()._auto_init() - - @api.model_create_multi - def create(self, vals_list): - lines = super().create(vals_list) - # Do not generate task/project when expense SO line, but allow - # generate task with hours=0. - for line in lines: - if line.state == 'sale' and not line.is_expense: - line.sudo()._timesheet_service_generation() - # if the SO line created a task, post a message on the order - if line.task_id: - msg_body = _("Task Created (%s): %s") % (line.product_id.name, line.task_id.id, line.task_id.name) - line.order_id.message_post(body=msg_body) - return lines - - def write(self, values): - result = super().write(values) - # changing the ordered quantity should change the planned hours on the - # task, whatever the SO state. It will be blocked by the super in case - # 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 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 - - ########################################### - # Service : Project and task generation - ########################################### - - def _convert_qty_company_hours(self, dest_company): - return self.product_uom_qty - - def _timesheet_create_project_prepare_values(self): - """Generate project values""" - account = self.order_id.analytic_account_id - if not account: - service_products = self.order_id.order_line.product_id.filtered(lambda p: p.type == 'service' and p.default_code) - default_code = service_products.default_code if len(service_products) == 1 else None - self.order_id._create_analytic_account(prefix=default_code) - account = self.order_id.analytic_account_id - - # create the project or duplicate one - return { - 'name': '%s - %s' % (self.order_id.client_order_ref, self.order_id.name) if self.order_id.client_order_ref else self.order_id.name, - 'analytic_account_id': account.id, - 'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)], - 'partner_id': self.order_id.partner_id.id, - 'sale_line_id': self.id, - 'active': True, - 'company_id': self.company_id.id, - } - - def _timesheet_create_project(self): - """ Generate project for the given so line, and link it. - :param project: record of project.project in which the task should be created - :return task: record of the created task - """ - self.ensure_one() - values = self._timesheet_create_project_prepare_values() - if self.product_id.project_template_id: - values['name'] = "%s - %s" % (values['name'], self.product_id.project_template_id.name) - project = self.product_id.project_template_id.copy(values) - project.tasks.write({ - 'sale_line_id': self.id, - 'partner_id': self.order_id.partner_id.id, - 'email_from': self.order_id.partner_id.email, - }) - # duplicating a project doesn't set the SO on sub-tasks - project.tasks.filtered(lambda task: task.parent_id != False).write({ - 'sale_line_id': self.id, - 'sale_order_id': self.order_id, - }) - else: - project_only_sol_count = self.env['sale.order.line'].search_count([ - ('order_id', '=', self.order_id.id), - ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), - ]) - if project_only_sol_count == 1: - values['name'] = "%s - [%s] %s" % (values['name'], self.product_id.default_code, self.product_id.name) if self.product_id.default_code else "%s - %s" % (values['name'], self.product_id.name) - project = self.env['project.project'].create(values) - - # Avoid new tasks to go to 'Undefined Stage' - if not project.type_ids: - project.type_ids = self.env['project.task.type'].create({'name': _('New')}) - - # link project as generated by current so line - self.write({'project_id': project.id}) - return project - - def _timesheet_create_task_prepare_values(self, project): - self.ensure_one() - planned_hours = self._convert_qty_company_hours(self.company_id) - sale_line_name_parts = self.name.split('\n') - title = sale_line_name_parts[0] or self.product_id.name - description = '
'.join(sale_line_name_parts[1:]) - return { - 'name': title if project.sale_line_id else '%s - %s' % (self.order_id.name or '', title), - 'planned_hours': planned_hours, - 'partner_id': self.order_id.partner_id.id, - 'email_from': self.order_id.partner_id.email, - 'description': description, - 'project_id': project.id, - 'sale_line_id': self.id, - 'sale_order_id': self.order_id.id, - 'company_id': project.company_id.id, - 'user_ids': False, # force non assigned task, as created as sudo() - } - - def _timesheet_create_task(self, project): - """ Generate task for the given so line, and link it. - :param project: record of project.project in which the task should be created - :return task: record of the created task - """ - values = self._timesheet_create_task_prepare_values(project) - task = self.env['project.task'].sudo().create(values) - self.write({'task_id': task.id}) - # post message on task - task_msg = _("This task has been created from: %s (%s)") % (self.order_id.id, self.order_id.name, self.product_id.name) - task.message_post(body=task_msg) - return task - - def _timesheet_service_generation(self): - """ For service lines, create the task or the project. If already exists, it simply links - the existing one to the line. - Note: If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a - new project/task. This explains the searches on 'sale_line_id' on project/task. This also - implied if so line of generated task has been modified, we may regenerate it. - """ - so_line_task_global_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_global_project') - so_line_new_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking in ['project_only', 'task_in_project']) - - # search so lines from SO of current so lines having their project generated, in order to check if the current one can - # create its own project, or reuse the one of its order. - map_so_project = {} - if so_line_new_project: - order_ids = self.mapped('order_id').ids - so_lines_with_project = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '=', False)]) - map_so_project = {sol.order_id.id: sol.project_id for sol in so_lines_with_project} - so_lines_with_project_templates = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '!=', False)]) - map_so_project_templates = {(sol.order_id.id, sol.product_id.project_template_id.id): sol.project_id for sol in so_lines_with_project_templates} - - # search the global project of current SO lines, in which create their task - map_sol_project = {} - if so_line_task_global_project: - map_sol_project = {sol.id: sol.product_id.with_company(sol.company_id).project_id for sol in so_line_task_global_project} - - def _can_create_project(sol): - if not sol.project_id: - if sol.product_id.project_template_id: - return (sol.order_id.id, sol.product_id.project_template_id.id) not in map_so_project_templates - elif sol.order_id.id not in map_so_project: - return True - return False - - def _determine_project(so_line): - """Determine the project for this sale order line. - Rules are different based on the service_tracking: - - - 'project_only': the project_id can only come from the sale order line itself - - 'task_in_project': the project_id comes from the sale order line only if no project_id was configured - on the parent sale order""" - - if so_line.product_id.service_tracking == 'project_only': - return so_line.project_id - elif so_line.product_id.service_tracking == 'task_in_project': - return so_line.order_id.project_id or so_line.project_id - - return False - - # task_global_project: create task in global project - for so_line in so_line_task_global_project: - if not so_line.task_id: - if map_sol_project.get(so_line.id) and so_line.product_uom_qty > 0: - so_line._timesheet_create_task(project=map_sol_project[so_line.id]) - - # project_only, task_in_project: create a new project, based or not on a template (1 per SO). May be create a task too. - # if 'task_in_project' and project_id configured on SO, use that one instead - for so_line in so_line_new_project: - project = _determine_project(so_line) - if not project and _can_create_project(so_line): - project = so_line._timesheet_create_project() - if so_line.product_id.project_template_id: - map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project - else: - map_so_project[so_line.order_id.id] = project - elif not project: - # Attach subsequent SO lines to the created project - so_line.project_id = ( - map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id)) - or map_so_project.get(so_line.order_id.id) - ) - if so_line.product_id.service_tracking == 'task_in_project': - if not project: - if so_line.product_id.project_template_id: - project = map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] - else: - project = map_so_project[so_line.order_id.id] - if not so_line.task_id: - so_line._timesheet_create_task(project=project) - - def _prepare_invoice_line(self, **optional_values): - """ - If the sale order line isn't linked to a sale order which already have a default analytic account, - this method allows to retrieve the analytic account which is linked to project or task directly linked - to this sale order line, or the analytic account of the project which uses this sale order line, if it exists. - """ - values = super(SaleOrderLine, self)._prepare_invoice_line(**optional_values) - if not values.get('analytic_account_id'): - if self.task_id.analytic_account_id: - values['analytic_account_id'] = self.task_id._get_task_analytic_account_id().id - elif self.project_id.analytic_account_id: - values['analytic_account_id'] = self.project_id.analytic_account_id.id - elif self.is_service and not self.is_expense: - task_analytic_account_id = self.env['project.task'].read_group([ - ('sale_line_id', '=', self.id), - ('analytic_account_id', '!=', False), - ], ['analytic_account_id'], ['analytic_account_id']) - project_analytic_account_id = self.env['project.project'].read_group([ - ('analytic_account_id', '!=', False), - '|', - ('sale_line_id', '=', self.id), - '&', - ('tasks.sale_line_id', '=', self.id), - ('tasks.analytic_account_id', '=', False) - ], ['analytic_account_id'], ['analytic_account_id']) - analytic_account_ids = {rec['analytic_account_id'][0] for rec in (task_analytic_account_id + project_analytic_account_id)} - if len(analytic_account_ids) == 1: - values['analytic_account_id'] = analytic_account_ids.pop() - if self.task_id.analytic_tag_ids: - values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids] - if self.task_id.analytic_tag_ids or self.task_id.project_id.analytic_tag_ids: - values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids | self.task_id.project_id.analytic_tag_ids] - elif self.is_service and not self.is_expense: - tag_ids = self.env['account.analytic.tag'].search([ - '|', ('task_ids.sale_line_id', '=', self.id), ('project_ids.sale_line_id', '=', self.id) - ]) - values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in tag_ids] - return values diff --git a/addons/sale_project/models/sale_order_line.py b/addons/sale_project/models/sale_order_line.py new file mode 100644 index 00000000000..ef8d50484c7 --- /dev/null +++ b/addons/sale_project/models/sale_order_line.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, Command, fields, models, _ +from odoo.tools.sql import column_exists, create_column + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + project_id = fields.Many2one( + 'project.project', 'Generated Project', + index=True, copy=False, help="Project generated by the sales order item") + task_id = fields.Many2one( + 'project.task', 'Generated Task', + index=True, copy=False, help="Task generated by the sales order item") + is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, compute_sudo=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.") + + @api.depends('product_id') + def _compute_is_service(self): + for so_line in self: + so_line.is_service = so_line.product_id.type == 'service' + + @api.depends('product_id') + def _compute_product_updatable(self): + for line in self: + if line.product_id.type == 'service' and line.state == 'sale': + line.product_updatable = False + else: + super(SaleOrderLine, line)._compute_product_updatable() + + def _auto_init(self): + """ + Create column to stop ORM from computing it himself (too slow) + """ + if not column_exists(self.env.cr, 'sale_order_line', 'is_service'): + create_column(self.env.cr, 'sale_order_line', 'is_service', 'bool') + self.env.cr.execute(""" + UPDATE sale_order_line line + SET is_service = (pt.type = 'service') + FROM product_product pp + LEFT JOIN product_template pt ON pt.id = pp.product_tmpl_id + WHERE pp.id = line.product_id + """) + return super()._auto_init() + + @api.model_create_multi + def create(self, vals_list): + lines = super().create(vals_list) + # Do not generate task/project when expense SO line, but allow + # generate task with hours=0. + for line in lines: + if line.state == 'sale' and not line.is_expense: + line.sudo()._timesheet_service_generation() + # if the SO line created a task, post a message on the order + if line.task_id: + msg_body = _("Task Created (%s): %s") % (line.product_id.name, line.task_id.id, line.task_id.name) + line.order_id.message_post(body=msg_body) + return lines + + def write(self, values): + result = super().write(values) + # changing the ordered quantity should change the planned hours on the + # task, whatever the SO state. It will be blocked by the super in case + # 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 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 + + ########################################### + # Service : Project and task generation + ########################################### + + def _convert_qty_company_hours(self, dest_company): + return self.product_uom_qty + + def _timesheet_create_project_prepare_values(self): + """Generate project values""" + account = self.order_id.analytic_account_id + if not account: + service_products = self.order_id.order_line.product_id.filtered(lambda p: p.type == 'service' and p.default_code) + default_code = service_products.default_code if len(service_products) == 1 else None + self.order_id._create_analytic_account(prefix=default_code) + account = self.order_id.analytic_account_id + + # create the project or duplicate one + return { + 'name': '%s - %s' % (self.order_id.client_order_ref, self.order_id.name) if self.order_id.client_order_ref else self.order_id.name, + 'analytic_account_id': account.id, + 'analytic_tag_ids': [Command.set(self.analytic_tag_ids.ids)], + 'partner_id': self.order_id.partner_id.id, + 'sale_line_id': self.id, + 'active': True, + 'company_id': self.company_id.id, + } + + def _timesheet_create_project(self): + """ Generate project for the given so line, and link it. + :param project: record of project.project in which the task should be created + :return task: record of the created task + """ + self.ensure_one() + values = self._timesheet_create_project_prepare_values() + if self.product_id.project_template_id: + values['name'] = "%s - %s" % (values['name'], self.product_id.project_template_id.name) + project = self.product_id.project_template_id.copy(values) + project.tasks.write({ + 'sale_line_id': self.id, + 'partner_id': self.order_id.partner_id.id, + 'email_from': self.order_id.partner_id.email, + }) + # duplicating a project doesn't set the SO on sub-tasks + project.tasks.filtered(lambda task: task.parent_id != False).write({ + 'sale_line_id': self.id, + 'sale_order_id': self.order_id, + }) + else: + project_only_sol_count = self.env['sale.order.line'].search_count([ + ('order_id', '=', self.order_id.id), + ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), + ]) + if project_only_sol_count == 1: + values['name'] = "%s - [%s] %s" % (values['name'], self.product_id.default_code, self.product_id.name) if self.product_id.default_code else "%s - %s" % (values['name'], self.product_id.name) + project = self.env['project.project'].create(values) + + # Avoid new tasks to go to 'Undefined Stage' + if not project.type_ids: + project.type_ids = self.env['project.task.type'].create({'name': _('New')}) + + # link project as generated by current so line + self.write({'project_id': project.id}) + return project + + def _timesheet_create_task_prepare_values(self, project): + self.ensure_one() + planned_hours = self._convert_qty_company_hours(self.company_id) + sale_line_name_parts = self.name.split('\n') + title = sale_line_name_parts[0] or self.product_id.name + description = '
'.join(sale_line_name_parts[1:]) + return { + 'name': title if project.sale_line_id else '%s - %s' % (self.order_id.name or '', title), + 'planned_hours': planned_hours, + 'partner_id': self.order_id.partner_id.id, + 'email_from': self.order_id.partner_id.email, + 'description': description, + 'project_id': project.id, + 'sale_line_id': self.id, + 'sale_order_id': self.order_id.id, + 'company_id': project.company_id.id, + 'user_ids': False, # force non assigned task, as created as sudo() + } + + def _timesheet_create_task(self, project): + """ Generate task for the given so line, and link it. + :param project: record of project.project in which the task should be created + :return task: record of the created task + """ + values = self._timesheet_create_task_prepare_values(project) + task = self.env['project.task'].sudo().create(values) + self.write({'task_id': task.id}) + # post message on task + task_msg = _("This task has been created from: %s (%s)") % (self.order_id.id, self.order_id.name, self.product_id.name) + task.message_post(body=task_msg) + return task + + def _timesheet_service_generation(self): + """ For service lines, create the task or the project. If already exists, it simply links + the existing one to the line. + Note: If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a + new project/task. This explains the searches on 'sale_line_id' on project/task. This also + implied if so line of generated task has been modified, we may regenerate it. + """ + so_line_task_global_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_global_project') + so_line_new_project = self.filtered(lambda sol: sol.is_service and sol.product_id.service_tracking in ['project_only', 'task_in_project']) + + # search so lines from SO of current so lines having their project generated, in order to check if the current one can + # create its own project, or reuse the one of its order. + map_so_project = {} + if so_line_new_project: + order_ids = self.mapped('order_id').ids + so_lines_with_project = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '=', False)]) + map_so_project = {sol.order_id.id: sol.project_id for sol in so_lines_with_project} + so_lines_with_project_templates = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ('product_id.service_tracking', 'in', ['project_only', 'task_in_project']), ('product_id.project_template_id', '!=', False)]) + map_so_project_templates = {(sol.order_id.id, sol.product_id.project_template_id.id): sol.project_id for sol in so_lines_with_project_templates} + + # search the global project of current SO lines, in which create their task + map_sol_project = {} + if so_line_task_global_project: + map_sol_project = {sol.id: sol.product_id.with_company(sol.company_id).project_id for sol in so_line_task_global_project} + + def _can_create_project(sol): + if not sol.project_id: + if sol.product_id.project_template_id: + return (sol.order_id.id, sol.product_id.project_template_id.id) not in map_so_project_templates + elif sol.order_id.id not in map_so_project: + return True + return False + + def _determine_project(so_line): + """Determine the project for this sale order line. + Rules are different based on the service_tracking: + + - 'project_only': the project_id can only come from the sale order line itself + - 'task_in_project': the project_id comes from the sale order line only if no project_id was configured + on the parent sale order""" + + if so_line.product_id.service_tracking == 'project_only': + return so_line.project_id + elif so_line.product_id.service_tracking == 'task_in_project': + return so_line.order_id.project_id or so_line.project_id + + return False + + # task_global_project: create task in global project + for so_line in so_line_task_global_project: + if not so_line.task_id: + if map_sol_project.get(so_line.id) and so_line.product_uom_qty > 0: + so_line._timesheet_create_task(project=map_sol_project[so_line.id]) + + # project_only, task_in_project: create a new project, based or not on a template (1 per SO). May be create a task too. + # if 'task_in_project' and project_id configured on SO, use that one instead + for so_line in so_line_new_project: + project = _determine_project(so_line) + if not project and _can_create_project(so_line): + project = so_line._timesheet_create_project() + if so_line.product_id.project_template_id: + map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project + else: + map_so_project[so_line.order_id.id] = project + elif not project: + # Attach subsequent SO lines to the created project + so_line.project_id = ( + map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id)) + or map_so_project.get(so_line.order_id.id) + ) + if so_line.product_id.service_tracking == 'task_in_project': + if not project: + if so_line.product_id.project_template_id: + project = map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] + else: + project = map_so_project[so_line.order_id.id] + if not so_line.task_id: + so_line._timesheet_create_task(project=project) + + def _prepare_invoice_line(self, **optional_values): + """ + If the sale order line isn't linked to a sale order which already have a default analytic account, + this method allows to retrieve the analytic account which is linked to project or task directly linked + to this sale order line, or the analytic account of the project which uses this sale order line, if it exists. + """ + values = super(SaleOrderLine, self)._prepare_invoice_line(**optional_values) + if not values.get('analytic_account_id'): + if self.task_id.analytic_account_id: + values['analytic_account_id'] = self.task_id._get_task_analytic_account_id().id + elif self.project_id.analytic_account_id: + values['analytic_account_id'] = self.project_id.analytic_account_id.id + elif self.is_service and not self.is_expense: + task_analytic_account_id = self.env['project.task'].read_group([ + ('sale_line_id', '=', self.id), + ('analytic_account_id', '!=', False), + ], ['analytic_account_id'], ['analytic_account_id']) + project_analytic_account_id = self.env['project.project'].read_group([ + ('analytic_account_id', '!=', False), + '|', + ('sale_line_id', '=', self.id), + '&', + ('tasks.sale_line_id', '=', self.id), + ('tasks.analytic_account_id', '=', False) + ], ['analytic_account_id'], ['analytic_account_id']) + analytic_account_ids = {rec['analytic_account_id'][0] for rec in (task_analytic_account_id + project_analytic_account_id)} + if len(analytic_account_ids) == 1: + values['analytic_account_id'] = analytic_account_ids.pop() + if self.task_id.analytic_tag_ids: + values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids] + if self.task_id.analytic_tag_ids or self.task_id.project_id.analytic_tag_ids: + values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in self.task_id.analytic_tag_ids | self.task_id.project_id.analytic_tag_ids] + elif self.is_service and not self.is_expense: + tag_ids = self.env['account.analytic.tag'].search([ + '|', ('task_ids.sale_line_id', '=', self.id), ('project_ids.sale_line_id', '=', self.id) + ]) + values['analytic_tag_ids'] += [Command.link(tag_id.id) for tag_id in tag_ids] + return values