diff --git a/addons/project/views/project_task_views.xml b/addons/project/views/project_task_views.xml index bc6987d1db4..938062446c5 100644 --- a/addons/project/views/project_task_views.xml +++ b/addons/project/views/project_task_views.xml @@ -859,6 +859,30 @@ + + project.task.kanban.sale.order + project.task + + primary + + + project.quick_create_task_form_inherit_view_default_project + + + + + + project.task.form.quick.create.sale.order.inherited + project.task + + primary + + + 0 + + + + My Tasks project.task diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index ea011daee86..8d06ce573ed 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -579,6 +579,18 @@ class Project(models.Model): return not self.allow_billable def action_view_tasks(self): + if self.env.context.get('generate_milestone'): + line_id = self.env.context.get('default_sale_line_id') + default_line = self.env['sale.order.line'].browse(line_id) + milestone = self.env['project.milestone'].create({ + 'name': default_line.name, + 'project_id': self.id, + 'sale_line_id': line_id, + 'quantity_percentage': 1, + }) + if default_line.product_id.service_tracking == 'task_in_project': + default_line.task_id.milestone_id = milestone.id + action = super().action_view_tasks() action['context']['hide_partner'] = self._get_hide_partner() return action diff --git a/addons/sale_project/models/sale_order.py b/addons/sale_project/models/sale_order.py index 78043d67079..d28ce045b1d 100644 --- a/addons/sale_project/models/sale_order.py +++ b/addons/sale_project/models/sale_order.py @@ -21,6 +21,9 @@ class SaleOrder(models.Model): project_count = fields.Integer(string='Number of Projects', compute='_compute_project_ids', groups='project.group_project_user') milestone_count = fields.Integer(compute='_compute_milestone_count') is_product_milestone = fields.Boolean(compute='_compute_is_product_milestone') + show_create_project_button = fields.Boolean(compute='_compute_show_project_and_task_button', groups='project.group_project_user') + show_project_button = fields.Boolean(compute='_compute_show_project_and_task_button', groups='project.group_project_user') + show_task_button = fields.Boolean(compute='_compute_show_project_and_task_button', groups='project.group_project_user') def _compute_milestone_count(self): read_group = self.env['project.milestone']._read_group( @@ -36,6 +39,18 @@ class SaleOrder(models.Model): for order in self: order.is_product_milestone = order.order_line.product_id.filtered(lambda p: p.service_policy == 'delivered_milestones') + def _compute_show_project_and_task_button(self): + is_project_manager = self.env.user.has_group('project.group_project_manager') + show_button_ids = self.env['sale.order.line']._read_group([ + ('order_id', 'in', self.ids), + ('order_id.state', 'not in', ['draft', 'sent']), + ('product_id.detailed_type', '=', 'service'), + ], aggregates=['order_id:array_agg'])[0][0] + for order in self: + order.show_project_button = order.id in show_button_ids and order.project_count + order.show_task_button = order.show_project_button or order.tasks_count + order.show_create_project_button = is_project_manager and order.id in show_button_ids and not order.project_count + @api.depends('order_line.product_id.project_id') def _compute_tasks_ids(self): for order in self: @@ -62,6 +77,7 @@ class SaleOrder(models.Model): projects = order.order_line.mapped('product_id.project_id') projects |= order.order_line.mapped('project_id') projects |= order.project_id + projects = projects.filtered('active') projects |= projects_per_so[order.id or order._origin.id] if not is_project_manager: projects = projects._filter_access_rules('read') @@ -88,56 +104,84 @@ class SaleOrder(models.Model): def action_view_task(self): self.ensure_one() + if not self.order_line: + return {'type': 'ir.actions.act_window_close'} list_view_id = self.env.ref('project.view_task_tree2').id form_view_id = self.env.ref('project.view_task_form2').id + kanban_view_id = self.env.ref('project.view_task_kanban_inherit_view_default_project').id - action = {'type': 'ir.actions.act_window_close'} - task_projects = self.tasks_ids.mapped('project_id') - if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...) - action = self.with_context(active_id=task_projects.id).env['ir.actions.actions']._for_xml_id( - 'project.act_project_project_2_project_task_all') - action['domain'] = [('id', 'in', self.tasks_ids.ids)] - if action.get('context'): - eval_context = self.env['ir.actions.actions']._get_eval_context() - eval_context.update({'active_id': task_projects.id}) - action_context = safe_eval(action['context'], eval_context) - action_context.update(eval_context) - action['context'] = action_context - else: - action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_task") - action['context'] = {} # erase default context to avoid default filter - if len(self.tasks_ids) > 1: # cross project kanban task - action['views'] = [[False, 'kanban'], [list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'calendar'], [False, 'pivot']] - elif len(self.tasks_ids) == 1: # single task -> form view - action['views'] = [(form_view_id, 'form')] - action['res_id'] = self.tasks_ids.id - # filter on the task of the current SO - action.setdefault('context', {}) - action['context'].update({'search_default_sale_order_id': self.id}) + action = self.env["ir.actions.actions"]._for_xml_id("project.action_view_task") + if self.tasks_count > 1: # cross project kanban task + action['views'] = [[kanban_view_id, 'kanban'], [list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'calendar'], [False, 'pivot']] + else: # 1 or 0 tasks -> form view + action['views'] = [(form_view_id, 'form')] + action['res_id'] = self.tasks_ids.id + # set default project + default_line = next(sol for sol in self.order_line if sol.product_id.detailed_type == 'service') + default_project_id = default_line.project_id.id or self.project_id.id or self.project_ids[:1].id + + action['context'] = { + 'search_default_sale_order_id': self.id, + 'default_sale_order_id': self.id, + 'default_sale_line_id': default_line.id, + 'default_partner_id': self.partner_id.id, + 'default_project_id': default_project_id, + 'default_user_ids': [self.env.uid], + } return action + def action_create_project(self): + self.ensure_one() + if not self.order_line: + return {'type': 'ir.actions.act_window_close'} + + sorted_line = self.order_line.sorted('sequence') + default_sale_line = next(sol for sol in sorted_line if sol.product_id.detailed_type == 'service') + return { + **self.env["ir.actions.actions"]._for_xml_id("project.open_create_project"), + 'context': { + 'default_sale_order_id': self.id, + 'default_sale_line_id': default_sale_line.id, + 'default_partner_id': self.partner_id.id, + 'default_user_ids': [self.env.uid], + 'default_allow_billable': 1, + 'hide_allow_billable': True, + 'default_company_id': self.company_id.id, + 'generate_milestone': default_sale_line.product_id.service_policy == 'delivered_milestones', + }, + } + def action_view_project_ids(self): self.ensure_one() - view_form_id = self.env.ref('project.edit_project').id - view_kanban_id = self.env.ref('project.view_project_kanban').id + if not self.order_line: + return {'type': 'ir.actions.act_window_close'} + + sorted_line = self.order_line.sorted('sequence') + default_sale_line = next(sol for sol in sorted_line if sol.product_id.detailed_type == 'service') action = { 'type': 'ir.actions.act_window', - 'domain': [('id', 'in', self.project_ids.ids)], - 'view_mode': 'kanban,form', 'name': _('Projects'), + 'domain': ['|', ('sale_order_id', '=', self.id), ('id', 'in', self.project_ids.ids)], 'res_model': 'project.project', + 'views': [(False, 'kanban'), (False, 'tree'), (False, 'form')], + 'view_mode': 'kanban,tree,form', + 'context': { + **self._context, + 'default_partner_id': self.partner_id.id, + 'default_sale_line_id': default_sale_line.id, + 'default_allow_billable': 1, + } } if len(self.project_ids) == 1: - action.update({'views': [(view_form_id, 'form')], 'res_id': self.project_ids.id}) - else: - action['views'] = [(view_kanban_id, 'kanban'), (view_form_id, 'form')] + action.update({'views': [(False, 'form')], 'res_id': self.project_ids.id}) return action def action_view_milestone(self): self.ensure_one() default_project = self.project_ids and self.project_ids[0] - default_sale_line = default_project.sale_line_id or self.order_line and self.order_line[0] + sorted_line = self.order_line.sorted('sequence') + default_sale_line = next(sol for sol in sorted_line if sol.is_service and sol.product_id.service_policy == 'delivered_milestones') return { 'type': 'ir.actions.act_window', 'name': _('Milestones'), @@ -154,8 +198,8 @@ class SaleOrder(models.Model): """), 'context': { **self.env.context, - 'default_project_id' : default_project.id, - 'default_sale_line_id' : default_sale_line.id, + 'default_project_id': default_project.id, + 'default_sale_line_id': default_sale_line.id, } } diff --git a/addons/sale_project/tests/common.py b/addons/sale_project/tests/common.py index b1ff625a529..6376ae6cf90 100644 --- a/addons/sale_project/tests/common.py +++ b/addons/sale_project/tests/common.py @@ -118,7 +118,44 @@ class TestSaleProjectCommon(TestSaleCommon): 'taxes_id': False, 'property_account_income_id': cls.account_sale.id, }) - + price_vals = { + 'standard_price': 11, + 'list_price': 13, + } + service_vals = { + 'type': 'service', + 'service_tracking': 'no', + 'project_id': False, + } + ( + cls.product_service_ordered_prepaid, + cls.product_service_delivered_milestone, + cls.product_service_delivered_manual, + cls.product_consumable, + ) = cls.env['product.product'].create([{ + 'name': "Service prepaid", + **price_vals, + **service_vals, + 'invoice_policy': 'order', + 'service_type': 'manual', + }, { + 'name': "Service milestone", + **price_vals, + **service_vals, + 'invoice_policy': 'delivery', + 'service_type': 'milestones', + }, { + 'name': "Service manual", + **price_vals, + **service_vals, + 'invoice_policy': 'delivery', + 'service_type': 'manual', + }, { + 'name': "Consumable", + **price_vals, + 'type': 'consu', + 'invoice_policy': 'order', + }]) # -- devliered_milestones (delivered, milestones) product_milestone_vals = { 'type': 'service', diff --git a/addons/sale_project/tests/test_sale_project.py b/addons/sale_project/tests/test_sale_project.py index 1d27654e77a..369842fc1a1 100644 --- a/addons/sale_project/tests/test_sale_project.py +++ b/addons/sale_project/tests/test_sale_project.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests.common import TransactionCase, users +from odoo.tests.common import new_test_user +from .common import TestSaleProjectCommon +from odoo.tests.common import tagged - -class TestSaleProject(TransactionCase): +@tagged('post_install', '-at_install') +class TestSaleProject(TestSaleProjectCommon): @classmethod def setUpClass(cls): @@ -207,14 +209,12 @@ class TestSaleProject(TransactionCase): self.product_order_service3.type = 'service' self.assertTrue(sale_order_line.is_service, "As the product is a service, the SOL should be a service") - @users('demo') def test_cancel_so_linked_to_project(self): """ Test that cancelling a SO linked to a project will not raise an error """ # Ensure user don't have edit right access to the project group_sale_manager = self.env.ref('sales_team.group_sale_manager') group_project_user = self.env.ref('project.group_project_user') self.env.user.write({'groups_id': [(6, 0, [group_sale_manager.id, group_project_user.id])]}) - sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({ 'partner_id': self.partner.id, 'partner_invoice_id': self.partner.id, @@ -230,7 +230,83 @@ class TestSaleProject(TransactionCase): sale_order.action_confirm() self.assertEqual(self.project_global.tasks.sale_line_id.id, sale_order_line.id, "The project tasks should be linked to the SOL from the SO") - - self.project_global.sale_line_id = sale_order_line + #use of sudo() since the env.user does not have the access right to edit projects. + self.project_global.sudo().sale_line_id = sale_order_line sale_order.with_context({'disable_cancel_warning': True}).action_cancel() self.assertFalse(self.project_global.sale_line_id, "The project should not be linked to the SOL anymore") + + def test_compute_project_and_task_button(self): + """ + Ensures that when a sale order has at least one sol with a service product whose service-policy is either delivered on milestone or ordered prepaid, then the + show_create_project_button is set to True. If the sale order also has one project linked to it, then the show_project_and_task_button should be True, and the show_create_button + should be updated to False + """ + sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create({ + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + }) + sale_order_2 = sale_order_1.copy() + sale_order_3 = sale_order_1.copy() + (sale_order_1 | sale_order_2 | sale_order_3).action_confirm() + # consumable product, manual service product + self.env['sale.order.line'].create([{ + 'product_id': self.product_consumable.id, + 'order_id': sale_order_1.id, + }]) + self.assertFalse(sale_order_1.show_create_project_button, "There is no service product with one of the correct service_policy on the sale order, the button should be hidden") + self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden") + # add a milestone product + line_delivered_milestone = self.env['sale.order.line'].create({ + 'product_id': self.product_service_delivered_milestone.id, + 'order_id': sale_order_1.id, + }) + # the user does not have project creation right + user_wrong_group = new_test_user(self.env, groups='sales_team.group_sale_manager,project.group_project_user', login='wendy', name='wendy') + sale_order_1.with_user(user_wrong_group)._compute_show_project_and_task_button() + self.assertFalse(sale_order_1.show_create_project_button, "The user does not have the right to create a new project, the button should be hidden") + self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden") + # the user has project creation right + sale_order_1._compute_show_project_and_task_button() + self.assertTrue(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on milestone' on the sale order, the button should be displayed") + self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden") + # add a project to the SO + line_delivered_milestone.project_id = self.project_global + sale_order_1._compute_show_project_and_task_button() + self.assertFalse(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on milestone' and a project on the sale order, the button should be hidden") + self.assertTrue(sale_order_1.show_project_button, "There is a product service with the service_policy set on 'delivered on milestone' and a project on the sale order, the button should be displayed") + self.assertTrue(sale_order_1.show_task_button, "There is a product service with the service_policy set on 'delivered on milestone' and a project on the sale order, the button should be displayed") + + # add an ordered_prepaid service product + line_prepaid = self.env['sale.order.line'].create({ + 'product_id': self.product_service_ordered_prepaid.id, + 'order_id': sale_order_2.id, + }) + sale_order_2._compute_show_project_and_task_button() + self.assertTrue(sale_order_2.show_create_project_button, "There is a product service with the service_policy set on 'ordered_prepaid' on the sale order, the button should be displayed") + self.assertFalse(sale_order_2.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertFalse(sale_order_2.show_task_button, "There is no project on the sale order, the button should be hidden") + # create a new task, whose sale order item is a sol of the SO + self.env['project.task'].create({ + 'name': 'Test Task', + 'project_id': self.project_global.id, + 'sale_line_id': line_prepaid.id, + }) + sale_order_2._compute_tasks_ids() + sale_order_2._compute_show_project_and_task_button() + self.assertTrue(sale_order_2.show_create_project_button, "There is a product service with the service_policy set on 'ordered_prepaid' on the sale order, the button should be displayed") + self.assertFalse(sale_order_2.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertTrue(sale_order_2.show_task_button, "There is no project on the sale order and there is a task whose sale item is one of the sale_line of the SO, the button should be displayed") + + # add a manual service product + self.env['sale.order.line'].create({ + 'product_id': self.product_service_delivered_manual.id, + 'order_id': sale_order_3.id, + }) + sale_order_3._compute_show_project_and_task_button() + self.assertTrue(sale_order_3.show_create_project_button, "There is a product service with the service_policy set on 'manual' on the sale order, the button should be displayed") + self.assertFalse(sale_order_3.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertFalse(sale_order_3.show_task_button, "There is no project on the sale order, the button should be hidden") diff --git a/addons/sale_project/views/sale_order_views.xml b/addons/sale_project/views/sale_order_views.xml index 34789b835dd..6ca1d2ddb99 100644 --- a/addons/sale_project/views/sale_order_views.xml +++ b/addons/sale_project/views/sale_order_views.xml @@ -7,16 +7,25 @@ 10 + + + + - - diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 0b4aa7ded2b..533b8ff42aa 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -20,6 +20,8 @@ class SaleOrder(models.Model): timesheet_total_duration = fields.Integer("Timesheet Total Duration", compute='_compute_timesheet_total_duration', help="Total recorded duration, expressed in the encoding UoM, and rounded to the unit", compute_sudo=True, groups="hr_timesheet.group_hr_timesheet_user") + show_hours_recorded_button = fields.Boolean(compute="_compute_show_hours_recorded_button", groups="hr_timesheet.group_hr_timesheet_user") + def _compute_timesheet_count(self): timesheets_per_so = { @@ -63,6 +65,21 @@ class SaleOrder(models.Model): # We want to display only one time the warning for each SOL upsellable_lines.write({'has_displayed_warning_upsell': True}) + def _compute_show_hours_recorded_button(self): + show_button_ids = self._get_order_with_valid_service_product() + for order in self: + order.show_hours_recorded_button = order.timesheet_count or order.project_count and order.id in show_button_ids + + def _get_order_with_valid_service_product(self): + return self.env['sale.order.line']._read_group([ + ('order_id', 'in', self.ids), + ('state', 'in', ['sale', 'done']), + ('is_service', '=', True), + '|', + ('product_id.service_type', 'not in', ['milestones', 'manual']), + ('product_id.invoice_policy', '!=', 'delivery'), + ], aggregates=['order_id:array_agg'])[0][0] + def _get_prepaid_service_lines_to_upsell(self): """ Retrieve all sols which need to display an upsell activity warning in the SO @@ -85,22 +102,38 @@ class SaleOrder(models.Model): def action_view_timesheet(self): self.ensure_one() + if not self.order_line: + return {'type': 'ir.actions.act_window_close'} + action = self.env["ir.actions.actions"]._for_xml_id("sale_timesheet.timesheet_action_from_sales_order") - action['context'] = { - 'search_default_billable_timesheet': True + default_sale_line = next(sale_line for sale_line in self.order_line if sale_line.is_service and sale_line.product_id.service_policy in ['ordered_prepaid', 'delivered_timesheet']) + context = { + 'search_default_billable_timesheet': True, + 'default_is_so_line_edited': True, + 'default_so_line': default_sale_line.id, } # erase default filters - if self.order_line: - tasks = self.order_line.task_id._filter_access_rules_python('write') - if tasks: - action['context']['default_task_id'] = tasks[0].id - else: - projects = self.order_line.project_id._filter_access_rules_python('write') - if projects: - action['context']['default_project_id'] = projects[0].id - if self.timesheet_count > 0: - action['domain'] = [('so_line', 'in', self.order_line.ids)] + + tasks = self.order_line.task_id._filter_access_rules_python('write') + if tasks: + context['default_task_id'] = tasks[0].id else: - action = {'type': 'ir.actions.act_window_close'} + projects = self.order_line.project_id._filter_access_rules_python('write') + if projects: + context['default_project_id'] = projects[0].id + elif self.project_ids: + context['default_project_id'] = self.project_ids[0].id + action.update({ + 'context': context, + 'domain': [('so_line', 'in', self.order_line.ids)], + 'help': _(""" +

+ No activities found. Let's start a new one! +

+ Track your working hours by projects every day and invoice this time to your customers. +

+ """) + }) + return action def _create_invoices(self, grouped=False, final=False, date=None): diff --git a/addons/sale_timesheet/tests/common.py b/addons/sale_timesheet/tests/common.py index 5d30672d5de..c5825314bc3 100644 --- a/addons/sale_timesheet/tests/common.py +++ b/addons/sale_timesheet/tests/common.py @@ -254,6 +254,16 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon): 'taxes_id': False, 'property_account_income_id': cls.account_sale.id, }) + cls.product_service_delivered_timesheet = cls.env['product.product'].create({ + 'name': "Service timesheet", + 'standard_price': 11, + 'list_price': 13, + 'type': 'service', + 'service_tracking': 'no', + 'project_id': False, + 'invoice_policy': 'delivery', + 'service_type': 'timesheet', + }) def setUp(self): super().setUp() diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index 232562115a0..7ecbec52cce 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -784,3 +784,92 @@ class TestSaleService(TestCommonSaleTimesheet): self.sale_order.action_confirm() project_allocated_hours = self.sale_order.order_line[0].project_id.allocated_hours self.assertEqual(30, project_allocated_hours, 'current set the project allocated hours.') + + def test_compute_project_and_task_button_with_ts(self): + """ This test ensures that the button are correctly computed when there is a timesheet service product on a SO. The behavior was not modified in sale_timesheet, but since + the timesheet product case can not be tested in sale_project, we have to add the test here.""" + sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create([{ + 'partner_id': self.partner_a.id, + 'partner_invoice_id': self.partner_a.id, + 'partner_shipping_id': self.partner_a.id, + }]) + # delivered timesheet + line_1 = self.env['sale.order.line'].create({ + 'product_id': self.product_service_delivered_timesheet.id, + 'order_id': sale_order_1.id, + }) + sale_order_1.action_confirm() + self.assertTrue(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' on the sale order, the button should be displayed") + self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden") + self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden") + line_1.project_id = self.project_global.id + sale_order_1._compute_show_project_and_task_button() + self.assertFalse(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be hidden") + self.assertTrue(sale_order_1.show_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed") + self.assertTrue(sale_order_1.show_task_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed") + + def test_compute_show_timesheet_button(self): + """ This test ensures that the hours recorded button is correctly computed. If there is a service product with an invoice policy of prepaid or timesheet, and there is + at least on project linked to the SO, then the button should be displayed """ + sale_order_1, sale_order_2 = self.env['sale.order'].with_context(tracking_disable=True).create([{ + 'partner_id': self.partner_a.id, + 'partner_invoice_id': self.partner_a.id, + 'partner_shipping_id': self.partner_a.id, + }, { + 'partner_id': self.partner_a.id, + 'partner_invoice_id': self.partner_a.id, + 'partner_shipping_id': self.partner_a.id, + }]) + # consumable product, delivered milestone, and delivered manual + self.env['sale.order.line'].create([{ + 'product_id': self.product_consumable.id, + 'order_id': sale_order_1.id, + }, { + 'product_id': self.product_service_delivered_milestone.id, + 'order_id': sale_order_1.id, + }, { + 'product_id': self.product_service_delivered_manual.id, + 'order_id': sale_order_1.id, + }]) + sale_order_1.action_confirm() + self.assertFalse(sale_order_1.show_hours_recorded_button, "There is no service product service with the correct service_policy set on 'delivered on timesheet' on the sale order, the button should be hidden") + # adds a delivered timesheet product to the SO + line_4 = self.env['sale.order.line'].create({ + 'product_id': self.product_service_delivered_timesheet.id, + 'order_id': sale_order_1.id, + }) + self.assertFalse(sale_order_1.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden") + line_4.project_id = self.project_global + sale_order_1._compute_show_hours_recorded_button() + self.assertTrue(sale_order_1.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed") + + line_1 = self.env['sale.order.line'].create({ + 'product_id': self.product_service_ordered_prepaid.id, + 'order_id': sale_order_2.id, + }) + sale_order_2.action_confirm() + self.assertFalse(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden") + line_1.project_id = self.project_global + sale_order_2._compute_show_hours_recorded_button() + self.assertTrue(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed") + # remove the project from the so and ensure the SO is back to its previous state + line_1.project_id = False + sale_order_2._compute_show_hours_recorded_button() + self.assertFalse(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden") + + # adds a task whose sale item is a sale order line from the SO, and adds a timesheet in that task. This should enable the display of the button + task = self.env['project.task'].create({ + 'name': 'Test Task', + 'project_id': self.project_global.id, + 'sale_line_id': line_1.id, + }) + self.env['account.analytic.line'].create({ + 'name': "timesheet", + 'unit_amount': 5, + 'project_id': task.project_id.id, + 'task_id': task.id, + 'employee_id': self.employee_user.id, + }) + sale_order_2._compute_timesheet_count() + sale_order_2._compute_show_hours_recorded_button() + self.assertTrue(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed") diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml index 80c6279ec26..002027ca137 100644 --- a/addons/sale_timesheet/views/project_task_views.xml +++ b/addons/sale_timesheet/views/project_task_views.xml @@ -38,7 +38,7 @@ - +