From e871370f6d1c9e519a45ff890c7098f4107e331c Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 30 Aug 2021 11:01:48 -0700 Subject: [PATCH 1/5] [ADD] sale_timesheet_work_entry_rate: for Odoo 14.0 --- sale_timesheet_work_entry_rate/__init__.py | 3 + .../__manifest__.py | 25 +++++ .../data/hr_timesheet_work_entry_demo.xml | 15 +++ .../models/__init__.py | 5 + sale_timesheet_work_entry_rate/models/sale.py | 59 ++++++++++++ .../models/timesheet.py | 9 ++ .../models/work_entry.py | 9 ++ .../tests/__init__.py | 3 + .../tests/test_sale_flow.py | 96 +++++++++++++++++++ .../views/timesheet_views.xml | 18 ++++ .../views/work_entry_views.xml | 15 +++ 11 files changed, 257 insertions(+) create mode 100644 sale_timesheet_work_entry_rate/__init__.py create mode 100644 sale_timesheet_work_entry_rate/__manifest__.py create mode 100644 sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml create mode 100644 sale_timesheet_work_entry_rate/models/__init__.py create mode 100644 sale_timesheet_work_entry_rate/models/sale.py create mode 100644 sale_timesheet_work_entry_rate/models/timesheet.py create mode 100644 sale_timesheet_work_entry_rate/models/work_entry.py create mode 100644 sale_timesheet_work_entry_rate/tests/__init__.py create mode 100644 sale_timesheet_work_entry_rate/tests/test_sale_flow.py create mode 100644 sale_timesheet_work_entry_rate/views/timesheet_views.xml create mode 100644 sale_timesheet_work_entry_rate/views/work_entry_views.xml diff --git a/sale_timesheet_work_entry_rate/__init__.py b/sale_timesheet_work_entry_rate/__init__.py new file mode 100644 index 00000000..09434554 --- /dev/null +++ b/sale_timesheet_work_entry_rate/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models diff --git a/sale_timesheet_work_entry_rate/__manifest__.py b/sale_timesheet_work_entry_rate/__manifest__.py new file mode 100644 index 00000000..2abb9061 --- /dev/null +++ b/sale_timesheet_work_entry_rate/__manifest__.py @@ -0,0 +1,25 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Timesheet Billing Rate', + 'version': '14.0.1.0.0', + 'category': 'Sale', + 'author': 'Hibou Corp.', + 'license': 'OPL-1', + 'website': 'https://hibou.io/', + 'depends': [ + 'hibou_professional', + 'hr_timesheet_work_entry', + 'sale_timesheet', + ], + 'data': [ + ], + 'demo': [ + 'data/hr_timesheet_work_entry_demo.xml', + 'views/timesheet_views.xml', + 'views/work_entry_views.xml', + ], + 'installable': True, + 'auto_install': True, + 'application': False, + } diff --git a/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml b/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml new file mode 100644 index 00000000..2ad38d6a --- /dev/null +++ b/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml @@ -0,0 +1,15 @@ + + + + + + + + + Double Time + TS_DOUBLE + + + + + \ No newline at end of file diff --git a/sale_timesheet_work_entry_rate/models/__init__.py b/sale_timesheet_work_entry_rate/models/__init__.py new file mode 100644 index 00000000..00c7883c --- /dev/null +++ b/sale_timesheet_work_entry_rate/models/__init__.py @@ -0,0 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import sale +from . import timesheet +from . import work_entry diff --git a/sale_timesheet_work_entry_rate/models/sale.py b/sale_timesheet_work_entry_rate/models/sale.py new file mode 100644 index 00000000..f98ad71f --- /dev/null +++ b/sale_timesheet_work_entry_rate/models/sale.py @@ -0,0 +1,59 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.osv import expression + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + @api.depends('analytic_line_ids.work_type_id') + def _compute_qty_delivered(self): + super(SaleOrderLine, self)._compute_qty_delivered() + + # Overridden to select work_type_id and do multiplication at the end + def _get_delivered_quantity_by_analytic(self, additional_domain): + """ Compute and write the delivered quantity of current SO lines, based on their related + analytic lines. + :param additional_domain: domain to restrict AAL to include in computation (required since timesheet is an AAL with a project ...) + """ + result = {} + + # avoid recomputation if no SO lines concerned + if not self: + return result + + # group analytic lines by product uom and so line + domain = expression.AND([[('so_line', 'in', self.ids)], additional_domain]) + data = self.env['account.analytic.line'].read_group( + domain, + ['so_line', 'unit_amount', 'product_uom_id', 'work_type_id'], ['product_uom_id', 'so_line', 'work_type_id'], lazy=False + ) + + # convert uom and sum all unit_amount of analytic lines to get the delivered qty of SO lines + # browse so lines and product uoms here to make them share the same prefetch + lines = self.browse([item['so_line'][0] for item in data]) + lines_map = {line.id: line for line in lines} + product_uom_ids = [item['product_uom_id'][0] for item in data if item['product_uom_id']] + product_uom_map = {uom.id: uom for uom in self.env['uom.uom'].browse(product_uom_ids)} + work_type_ids = [item['work_type_id'][0] for item in data if item['work_type_id']] + work_type_map = {work.id: work for work in self.env['hr.work.entry.type'].browse(work_type_ids)} + for item in data: + if not item['product_uom_id']: + continue + if not item['work_type_id']: + continue + so_line_id = item['so_line'][0] + so_line = lines_map[so_line_id] + result.setdefault(so_line_id, 0.0) + uom = product_uom_map.get(item['product_uom_id'][0]) + if so_line.product_uom.category_id == uom.category_id: + qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP') + else: + qty = item['unit_amount'] + + work = work_type_map.get(item['work_type_id'][0]) + qty *= work.timesheet_billing_rate or 0.0 + result[so_line_id] += qty + + return result diff --git a/sale_timesheet_work_entry_rate/models/timesheet.py b/sale_timesheet_work_entry_rate/models/timesheet.py new file mode 100644 index 00000000..799db068 --- /dev/null +++ b/sale_timesheet_work_entry_rate/models/timesheet.py @@ -0,0 +1,9 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class AccountAnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + work_billing_rate = fields.Float(related='work_type_id.timesheet_billing_rate', string='Billing Multiplier') diff --git a/sale_timesheet_work_entry_rate/models/work_entry.py b/sale_timesheet_work_entry_rate/models/work_entry.py new file mode 100644 index 00000000..800e124e --- /dev/null +++ b/sale_timesheet_work_entry_rate/models/work_entry.py @@ -0,0 +1,9 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields, models + + +class HrWorkEntryType(models.Model): + _inherit = 'hr.work.entry.type' + + timesheet_billing_rate = fields.Float(string='Timesheet Billing Multiplier', default=1.0) diff --git a/sale_timesheet_work_entry_rate/tests/__init__.py b/sale_timesheet_work_entry_rate/tests/__init__.py new file mode 100644 index 00000000..41510a81 --- /dev/null +++ b/sale_timesheet_work_entry_rate/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import test_sale_flow diff --git a/sale_timesheet_work_entry_rate/tests/test_sale_flow.py b/sale_timesheet_work_entry_rate/tests/test_sale_flow.py new file mode 100644 index 00000000..302c12c6 --- /dev/null +++ b/sale_timesheet_work_entry_rate/tests/test_sale_flow.py @@ -0,0 +1,96 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo.addons.sale_timesheet.tests.test_project_billing import TestProjectBilling + + +class TestSaleFlow(TestProjectBilling): + + # Mainly from test_billing_task_rate + # Additional tests at the bottom. + def test_billing_work_entry_rate(self): + Task = self.env['project.task'].with_context(tracking_disable=True) + Timesheet = self.env['account.analytic.line'] + + # set subtask project on task rate project + self.project_task_rate.write({'subtask_project_id': self.project_subtask.id}) + + # create a task + task = Task.with_context(default_project_id=self.project_task_rate.id).create({ + 'name': 'first task', + }) + task._onchange_project() + + self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task created in a project billed on 'task rate' should be linked to a SOL of the project") + self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project") + + # log timesheet on task + timesheet1 = Timesheet.create({ + 'name': 'Test Line', + 'project_id': task.project_id.id, + 'task_id': task.id, + 'unit_amount': 50, + 'employee_id': self.employee_manager.id, + }) + + self.assertEqual(self.project_task_rate.sale_line_id, timesheet1.so_line, "The timesheet should be linked to the SOL associated to the Employee manager in the map") + + # create a subtask + subtask = Task.with_context(default_project_id=self.project_task_rate.subtask_project_id.id).create({ + 'name': 'first subtask task', + 'parent_id': task.id, + }) + + self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask should have the same customer as the one from their mother") + + # log timesheet on subtask + timesheet2 = Timesheet.create({ + 'name': 'Test Line on subtask', + 'project_id': subtask.project_id.id, + 'task_id': subtask.id, + 'unit_amount': 50, + 'employee_id': self.employee_user.id, + }) + + self.assertEqual(subtask.project_id, timesheet2.project_id, "The timesheet is in the subtask project") + self.assertFalse(timesheet2.so_line, "The timesheet should not be linked to SOL as it's a non billable project") + + # move task and subtask into task rate project + task.write({ + 'project_id': self.project_employee_rate.id, + }) + task._onchange_project() + subtask.write({ + 'project_id': self.project_employee_rate.id, + }) + subtask._onchange_project() + + self.assertFalse(task.sale_line_id, "Task moved in a employee rate billable project have empty so line") + self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project") + + self.assertFalse(subtask.sale_line_id, "Subask moved in a employee rate billable project have empty so line") + self.assertEqual(subtask.partner_id, task.project_id.partner_id, "Subask created in a project billed on 'employee rate' should have the same customer as the one from the project") + + # Work Entry Type + task.write({ + 'project_id': self.project_task_rate.id, + }) + task._onchange_project() + subtask.write({ + 'project_id': self.project_task_rate.id, + }) + subtask._onchange_project() + default_work_entry_type = self.env.ref('hr_timesheet_work_entry.work_input_timesheet') + # Timesheets were for regular default 'Timesheet' type + self.assertEqual((timesheet1 + timesheet2).mapped('work_type_id'), default_work_entry_type) + # Line is set and total adds up to all of the timesheets. + self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task) + self.assertEqual(task.sale_line_id.qty_delivered, 100.0) + + double_rate_work_entry_type = self.env.ref('sale_timesheet_work_entry_rate.work_input_timesheet_double') + self.assertEqual(double_rate_work_entry_type.timesheet_billing_rate, 2.0) + + # Convert to double rate. + timesheet1.write({ + 'work_type_id': double_rate_work_entry_type.id, + }) + self.assertEqual(task.sale_line_id.qty_delivered, 150.0) diff --git a/sale_timesheet_work_entry_rate/views/timesheet_views.xml b/sale_timesheet_work_entry_rate/views/timesheet_views.xml new file mode 100644 index 00000000..761424bd --- /dev/null +++ b/sale_timesheet_work_entry_rate/views/timesheet_views.xml @@ -0,0 +1,18 @@ + + + + + project.task.form.inherited.inherit + project.task + + + + + + + + + + + + diff --git a/sale_timesheet_work_entry_rate/views/work_entry_views.xml b/sale_timesheet_work_entry_rate/views/work_entry_views.xml new file mode 100644 index 00000000..ddac159c --- /dev/null +++ b/sale_timesheet_work_entry_rate/views/work_entry_views.xml @@ -0,0 +1,15 @@ + + + + + hr.work.type.view.form.inherit + hr.work.entry.type + + + + + + + + + From 120a21da0db0564de075346ab3879588d5a99faa Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 30 Aug 2021 11:53:42 -0700 Subject: [PATCH 2/5] [IMP] sale_timesheet_work_entry_rate: show allowed work types only --- sale_timesheet_work_entry_rate/views/timesheet_views.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sale_timesheet_work_entry_rate/views/timesheet_views.xml b/sale_timesheet_work_entry_rate/views/timesheet_views.xml index 761424bd..8889c43f 100644 --- a/sale_timesheet_work_entry_rate/views/timesheet_views.xml +++ b/sale_timesheet_work_entry_rate/views/timesheet_views.xml @@ -7,10 +7,14 @@ - + - + From 7304d614e7b4bd144e1c64700900af458106d320 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 30 Aug 2021 14:05:19 -0700 Subject: [PATCH 3/5] [FIX] sale_timesheet_work_entry_rate: views shouldn't be in demo --- sale_timesheet_work_entry_rate/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sale_timesheet_work_entry_rate/__manifest__.py b/sale_timesheet_work_entry_rate/__manifest__.py index 2abb9061..6e789dd5 100644 --- a/sale_timesheet_work_entry_rate/__manifest__.py +++ b/sale_timesheet_work_entry_rate/__manifest__.py @@ -13,11 +13,11 @@ 'sale_timesheet', ], 'data': [ + 'views/timesheet_views.xml', + 'views/work_entry_views.xml', ], 'demo': [ 'data/hr_timesheet_work_entry_demo.xml', - 'views/timesheet_views.xml', - 'views/work_entry_views.xml', ], 'installable': True, 'auto_install': True, From 1bff9199fbf70fd06d98637192ca358b25f83969 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 2 Sep 2021 09:20:49 -0700 Subject: [PATCH 4/5] [IMP] sale_timesheet_work_entry_rate: unset should behave as it if were 1.0 multiplier --- sale_timesheet_work_entry_rate/__manifest__.py | 2 +- .../data/hr_timesheet_work_entry_demo.xml | 7 +++++++ sale_timesheet_work_entry_rate/models/sale.py | 12 ++++++++---- .../tests/test_sale_flow.py | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/sale_timesheet_work_entry_rate/__manifest__.py b/sale_timesheet_work_entry_rate/__manifest__.py index 6e789dd5..dad1c3e1 100644 --- a/sale_timesheet_work_entry_rate/__manifest__.py +++ b/sale_timesheet_work_entry_rate/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'Timesheet Billing Rate', - 'version': '14.0.1.0.0', + 'version': '14.0.1.0.1', 'category': 'Sale', 'author': 'Hibou Corp.', 'license': 'OPL-1', diff --git a/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml b/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml index 2ad38d6a..49f72be2 100644 --- a/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml +++ b/sale_timesheet_work_entry_rate/data/hr_timesheet_work_entry_demo.xml @@ -12,4 +12,11 @@ + + Non-Productive Time + TS_NON_PRODUCTIVE + + + + \ No newline at end of file diff --git a/sale_timesheet_work_entry_rate/models/sale.py b/sale_timesheet_work_entry_rate/models/sale.py index f98ad71f..75963dab 100644 --- a/sale_timesheet_work_entry_rate/models/sale.py +++ b/sale_timesheet_work_entry_rate/models/sale.py @@ -41,8 +41,13 @@ class SaleOrderLine(models.Model): for item in data: if not item['product_uom_id']: continue - if not item['work_type_id']: - continue + work_type_rate = False + if item['work_type_id']: + work_type_rate = work_type_map.get(item['work_type_id'][0]).timesheet_billing_rate + if work_type_rate is False: + # unset field should be 1.0 by default, you CAN set it to 0.0 if you'd like. + work_type_rate = 1.0 + so_line_id = item['so_line'][0] so_line = lines_map[so_line_id] result.setdefault(so_line_id, 0.0) @@ -52,8 +57,7 @@ class SaleOrderLine(models.Model): else: qty = item['unit_amount'] - work = work_type_map.get(item['work_type_id'][0]) - qty *= work.timesheet_billing_rate or 0.0 + qty *= work_type_rate result[so_line_id] += qty return result diff --git a/sale_timesheet_work_entry_rate/tests/test_sale_flow.py b/sale_timesheet_work_entry_rate/tests/test_sale_flow.py index 302c12c6..21d00490 100644 --- a/sale_timesheet_work_entry_rate/tests/test_sale_flow.py +++ b/sale_timesheet_work_entry_rate/tests/test_sale_flow.py @@ -94,3 +94,19 @@ class TestSaleFlow(TestProjectBilling): 'work_type_id': double_rate_work_entry_type.id, }) self.assertEqual(task.sale_line_id.qty_delivered, 150.0) + + # Ensure that a created timesheet WITHOUT a work entry type behaves + # the same as it would have before this module (e.g. for historic reasons) + timesheet2.write({ + 'work_type_id': False, + }) + self.assertEqual(task.sale_line_id.qty_delivered, 150.0) + + # Ensure we can bill zero even with above default. + zero_rate_work_entry_type = self.env.ref('sale_timesheet_work_entry_rate.work_input_timesheet_free') + self.assertEqual(zero_rate_work_entry_type.timesheet_billing_rate, 0.0) + + timesheet2.write({ + 'work_type_id': zero_rate_work_entry_type.id, + }) + self.assertEqual(task.sale_line_id.qty_delivered, 100.0) From 27fa96fad0b5978fc8acf17a7c1ea6f331df15ed Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 7 Oct 2021 13:55:30 -0700 Subject: [PATCH 5/5] [MIG] sale_timesheet_work_entry_rate: to Odoo 15.0 --- .../__manifest__.py | 2 +- .../tests/test_sale_flow.py | 52 +++++++------------ 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/sale_timesheet_work_entry_rate/__manifest__.py b/sale_timesheet_work_entry_rate/__manifest__.py index dad1c3e1..7b88a4f5 100644 --- a/sale_timesheet_work_entry_rate/__manifest__.py +++ b/sale_timesheet_work_entry_rate/__manifest__.py @@ -2,7 +2,7 @@ { 'name': 'Timesheet Billing Rate', - 'version': '14.0.1.0.1', + 'version': '15.0.1.0.0', 'category': 'Sale', 'author': 'Hibou Corp.', 'license': 'OPL-1', diff --git a/sale_timesheet_work_entry_rate/tests/test_sale_flow.py b/sale_timesheet_work_entry_rate/tests/test_sale_flow.py index 21d00490..01bd1cd8 100644 --- a/sale_timesheet_work_entry_rate/tests/test_sale_flow.py +++ b/sale_timesheet_work_entry_rate/tests/test_sale_flow.py @@ -11,17 +11,13 @@ class TestSaleFlow(TestProjectBilling): Task = self.env['project.task'].with_context(tracking_disable=True) Timesheet = self.env['account.analytic.line'] - # set subtask project on task rate project - self.project_task_rate.write({'subtask_project_id': self.project_subtask.id}) - # create a task task = Task.with_context(default_project_id=self.project_task_rate.id).create({ 'name': 'first task', }) - task._onchange_project() - self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task created in a project billed on 'task rate' should be linked to a SOL of the project") - self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project") + self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task, "Task created in a project billed on 'task rate' should be linked to a SOL containing a prepaid service product and the remaining hours of this SOL should be greater than 0.") + self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'task rate' should have the same customer as the one from the project") # log timesheet on task timesheet1 = Timesheet.create({ @@ -32,12 +28,13 @@ class TestSaleFlow(TestProjectBilling): 'employee_id': self.employee_manager.id, }) - self.assertEqual(self.project_task_rate.sale_line_id, timesheet1.so_line, "The timesheet should be linked to the SOL associated to the Employee manager in the map") + self.assertEqual(task.sale_line_id, timesheet1.so_line, "The timesheet should be linked to the SOL associated to the task since the pricing type of the project is task rate.") # create a subtask - subtask = Task.with_context(default_project_id=self.project_task_rate.subtask_project_id.id).create({ + subtask = Task.with_context(default_project_id=self.project_task_rate.id).create({ 'name': 'first subtask task', 'parent_id': task.id, + 'display_project_id': self.project_subtask.id, }) self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask should have the same customer as the one from their mother") @@ -45,46 +42,34 @@ class TestSaleFlow(TestProjectBilling): # log timesheet on subtask timesheet2 = Timesheet.create({ 'name': 'Test Line on subtask', - 'project_id': subtask.project_id.id, + 'project_id': subtask.display_project_id.id, 'task_id': subtask.id, 'unit_amount': 50, 'employee_id': self.employee_user.id, }) - - self.assertEqual(subtask.project_id, timesheet2.project_id, "The timesheet is in the subtask project") + self.assertEqual(subtask.display_project_id, timesheet2.project_id, "The timesheet is in the subtask project") self.assertFalse(timesheet2.so_line, "The timesheet should not be linked to SOL as it's a non billable project") # move task and subtask into task rate project task.write({ 'project_id': self.project_employee_rate.id, }) - task._onchange_project() subtask.write({ - 'project_id': self.project_employee_rate.id, + 'display_project_id': self.project_employee_rate.id, }) - subtask._onchange_project() - self.assertFalse(task.sale_line_id, "Task moved in a employee rate billable project have empty so line") - self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project") + self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task moved in a employee rate billable project should keep its SOL because the partner_id has not changed too.") + self.assertEqual(task.partner_id, self.project_task_rate.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from its initial project.") - self.assertFalse(subtask.sale_line_id, "Subask moved in a employee rate billable project have empty so line") - self.assertEqual(subtask.partner_id, task.project_id.partner_id, "Subask created in a project billed on 'employee rate' should have the same customer as the one from the project") + self.assertEqual(subtask.sale_line_id, subtask.parent_id.sale_line_id, "Subtask moved in a employee rate billable project should have the SOL of its parent since it keep its partner_id and this partner is different than the one in the destination project.") + self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask moved in a project billed on 'employee rate' should keep its initial customer, that is the one of its parent.") - # Work Entry Type - task.write({ - 'project_id': self.project_task_rate.id, - }) - task._onchange_project() - subtask.write({ - 'project_id': self.project_task_rate.id, - }) - subtask._onchange_project() default_work_entry_type = self.env.ref('hr_timesheet_work_entry.work_input_timesheet') # Timesheets were for regular default 'Timesheet' type self.assertEqual((timesheet1 + timesheet2).mapped('work_type_id'), default_work_entry_type) # Line is set and total adds up to all of the timesheets. self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task) - self.assertEqual(task.sale_line_id.qty_delivered, 100.0) + self.assertEqual(task.sale_line_id.qty_delivered, 50.0) double_rate_work_entry_type = self.env.ref('sale_timesheet_work_entry_rate.work_input_timesheet_double') self.assertEqual(double_rate_work_entry_type.timesheet_billing_rate, 2.0) @@ -93,20 +78,23 @@ class TestSaleFlow(TestProjectBilling): timesheet1.write({ 'work_type_id': double_rate_work_entry_type.id, }) - self.assertEqual(task.sale_line_id.qty_delivered, 150.0) + self.assertEqual(task.sale_line_id.qty_delivered, 100.0) # Ensure that a created timesheet WITHOUT a work entry type behaves # the same as it would have before this module (e.g. for historic reasons) + timesheet1.write({ + 'work_type_id': False, + }) timesheet2.write({ 'work_type_id': False, }) - self.assertEqual(task.sale_line_id.qty_delivered, 150.0) + self.assertEqual(task.sale_line_id.qty_delivered, 50.0) # Ensure we can bill zero even with above default. zero_rate_work_entry_type = self.env.ref('sale_timesheet_work_entry_rate.work_input_timesheet_free') self.assertEqual(zero_rate_work_entry_type.timesheet_billing_rate, 0.0) - timesheet2.write({ + timesheet1.write({ 'work_type_id': zero_rate_work_entry_type.id, }) - self.assertEqual(task.sale_line_id.qty_delivered, 100.0) + self.assertEqual(task.sale_line_id.qty_delivered, 0.0)