From 3890f8d69463fe1920b161c0dd35b4bdaf26764e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Thu, 13 Jul 2017 08:56:22 +0200 Subject: [PATCH] [REF] sale_timesheet: test suite improvements Tests of sale_timesheet were using many demo data. This is a practice we want to avoid. A common class setting up test data (product service, partner, ...) is created to help writting good test cases. All tests has been adapted, moved, and some were added to cover the new flow (new product configuration). --- addons/sale_timesheet/tests/__init__.py | 1 + addons/sale_timesheet/tests/common.py | 265 +++++++++++++++ .../sale_timesheet/tests/test_sale_service.py | 98 ++++-- .../tests/test_sale_timesheet.py | 305 +++++++++++++----- .../tests/test_timesheet_revenue.py | 5 +- 5 files changed, 560 insertions(+), 114 deletions(-) create mode 100644 addons/sale_timesheet/tests/common.py diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py index 9b3932ef5fb..c18361894ff 100644 --- a/addons/sale_timesheet/tests/__init__.py +++ b/addons/sale_timesheet/tests/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import common from . import test_sale_timesheet from . import test_sale_service from . import test_timesheet_revenue diff --git a/addons/sale_timesheet/tests/common.py b/addons/sale_timesheet/tests/common.py new file mode 100644 index 00000000000..e60291c61bb --- /dev/null +++ b/addons/sale_timesheet/tests/common.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import common + + +class CommonTest(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(CommonTest, cls).setUpClass() + + # Create accounts + cls.account_debit = cls.env['account.account'].create({ + 'code': 'X1012', + 'name': 'Debtors - (test)', + 'reconcile': True, + 'user_type_id': cls.env.ref('account.data_account_type_receivable').id, + }) + cls.account_credit = cls.env['account.account'].create({ + 'code': 'X1111', + 'name': 'Creditors - (test)', + 'reconcile': True, + 'user_type_id': cls.env.ref('account.data_account_type_payable').id, + }) + cls.account_sale = cls.env['account.account'].create({ + 'code': 'X2020', + 'name': 'Product Sales - (test)', + 'reconcile': True, + 'user_type_id': cls.env.ref('account.data_account_type_revenue').id, + }) + + # Create project + cls.project_global = cls.env['project.project'].create({ + 'name': 'Project for selling timesheets', + 'allow_timesheets': True, + }) + + # Create service products + + # -- ordered quantities (ordered, timesheet) + cls.product_order_timesheet1 = cls.env['product.product'].create({ + 'name': "Service Ordered, create no task", + 'standard_price': 11, + 'list_price': 13, + 'type': 'service', + 'invoice_policy': 'order', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-ORDERED1', + 'service_type': 'timesheet', + 'service_tracking': 'no', + 'project_id': False, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_order_timesheet2 = cls.env['product.product'].create({ + 'name': "Service Ordered, create task in global project", + 'standard_price': 30, + 'list_price': 90, + 'type': 'service', + 'invoice_policy': 'order', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-ORDERED2', + 'service_type': 'timesheet', + 'service_tracking': 'task_global_project', + 'project_id': cls.project_global.id, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_order_timesheet3 = cls.env['product.product'].create({ + 'name': "Service Ordered, create task in new project", + 'standard_price': 10, + 'list_price': 20, + 'type': 'service', + 'invoice_policy': 'order', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-ORDERED3', + 'service_type': 'timesheet', + 'service_tracking': 'task_new_project', + 'project_id': False, # will create a project + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_order_timesheet4 = cls.env['product.product'].create({ + 'name': "Service Ordered, create project only", + 'standard_price': 15, + 'list_price': 30, + 'type': 'service', + 'invoice_policy': 'order', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-ORDERED4', + 'service_type': 'timesheet', + 'service_tracking': 'project_only', + 'project_id': False, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + + # -- timesheet on tasks (delivered, timesheet) + cls.product_delivery_timesheet1 = cls.env['product.product'].create({ + 'name': "Service delivered, create no task", + 'standard_price': 11, + 'list_price': 13, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI1', + 'service_type': 'timesheet', + 'service_tracking': 'no', + 'project_id': False, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_delivery_timesheet2 = cls.env['product.product'].create({ + 'name': "Service delivered, create task in global project", + 'standard_price': 30, + 'list_price': 90, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI2', + 'service_type': 'timesheet', + 'service_tracking': 'task_global_project', + 'project_id': cls.project_global.id, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_delivery_timesheet3 = cls.env['product.product'].create({ + 'name': "Service delivered, create task in new project", + 'standard_price': 10, + 'list_price': 20, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI3', + 'service_type': 'timesheet', + 'service_tracking': 'task_new_project', + 'project_id': False, # will create a project + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_delivery_timesheet4 = cls.env['product.product'].create({ + 'name': "Service delivered, create project only", + 'standard_price': 15, + 'list_price': 30, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI4', + 'service_type': 'timesheet', + 'service_tracking': 'project_only', + 'project_id': False, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + + # -- milestons (delivered, manual) + cls.product_delivery_manual1 = cls.env['product.product'].create({ + 'name': "Service delivered, create no task", + 'standard_price': 11, + 'list_price': 13, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI1', + 'service_type': 'manual', + 'service_tracking': 'no', + 'project_id': False, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_delivery_manual2 = cls.env['product.product'].create({ + 'name': "Service delivered, create task in global project", + 'standard_price': 30, + 'list_price': 90, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI2', + 'service_type': 'manual', + 'service_tracking': 'task_global_project', + 'project_id': cls.project_global.id, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_delivery_manual3 = cls.env['product.product'].create({ + 'name': "Service delivered, create task in new project", + 'standard_price': 10, + 'list_price': 20, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI3', + 'service_type': 'manual', + 'service_tracking': 'task_new_project', + 'project_id': False, # will create a project + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + cls.product_delivery_manual4 = cls.env['product.product'].create({ + 'name': "Service delivered, create project only", + 'standard_price': 15, + 'list_price': 30, + 'type': 'service', + 'invoice_policy': 'delivery', + 'uom_id': cls.env.ref('product.product_uom_hour').id, + 'uom_po_id': cls.env.ref('product.product_uom_hour').id, + 'default_code': 'SERV-DELI4', + 'service_type': 'manual', + 'service_tracking': 'project_only', + 'project_id': False, + 'taxes_id': False, + 'property_account_income_id': cls.account_sale.id, + }) + + # Create pricelists + cls.pricelist_usd = cls.env['product.pricelist'].create({ + 'name': 'USD pricelist', + 'active': True, + 'currency_id': cls.env.ref('base.USD').id, + 'company_id': cls.env.user.company_id.id, + }) + cls.pricelist_eur = cls.env['product.pricelist'].create({ + 'name': 'EUR pricelist', + 'active': True, + 'currency_id': cls.env.ref('base.EUR').id, + 'company_id': cls.env.user.company_id.id, + }) + + # Create partners + cls.partner_usd = cls.env['res.partner'].create({ + 'name': 'Cool Partner in USD', + 'email': 'partner.usd@test.com', + 'property_product_pricelist': cls.pricelist_usd.id, + 'property_account_payable_id': cls.account_credit.id, + 'property_account_receivable_id': cls.account_debit.id, + }) + cls.partner_eur = cls.env['res.partner'].create({ + 'name': 'Cool partner in EUR', + 'email': 'partner.eur@test.com', + 'property_product_pricelist': cls.pricelist_eur.id, + 'property_account_payable_id': cls.account_credit.id, + 'property_account_receivable_id': cls.account_debit.id, + }) + + # Create employees + cls.employee_user = cls.env['hr.employee'].create({ + 'name': 'Employee User', + 'timesheet_cost': 15, + }) + cls.employee_manager = cls.env['hr.employee'].create({ + 'name': 'Employee Manager', + 'timesheet_cost': 45, + }) diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index c2f6f34c495..5e92fe92fa2 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -1,43 +1,51 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.sale.tests.test_sale_common import TestSale + +from odoo.addons.sale_timesheet.tests.common import CommonTest -class TestSaleService(TestSale): +class TestSaleService(CommonTest): + """ This test suite provide checks for miscellaneous small things. """ def test_sale_service(self): - """ Test task creation when confirming a so with the corresponding product """ - prod_task = self.env.ref('product.product_product_1') - so_vals = { - 'partner_id': self.partner.id, - 'partner_invoice_id': self.partner.id, - 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': prod_task.name, 'product_id': prod_task.id, 'product_uom_qty': 50, 'product_uom': prod_task.uom_id.id, 'price_unit': prod_task.list_price})], - 'pricelist_id': self.env.ref('product.list0').id, + """ Test task creation when confirming a sale_order with the corresponding product """ + sale_order_vals = { + 'partner_id': self.partner_usd.id, + 'partner_invoice_id': self.partner_usd.id, + 'partner_shipping_id': self.partner_usd.id, + 'order_line': [(0, 0, { + 'name': self.product_delivery_timesheet2.name, + 'product_id': self.product_delivery_timesheet2.id, + 'product_uom_qty': 50, + 'product_uom': self.product_delivery_timesheet2.uom_id.id, + 'price_unit': self.product_delivery_timesheet2.list_price + }), + ], + 'pricelist_id': self.pricelist_usd.id, } - so = self.env['sale.order'].create(so_vals) - so.action_confirm() - self.assertEqual(so.invoice_status, 'no', 'Sale Service: there should be nothing to invoice after validation') + sale_order = self.env['sale.order'].create(sale_order_vals) + sale_order.action_confirm() + self.assertEqual(sale_order.invoice_status, 'no', 'Sale Service: there should be nothing to invoice after validation') # check task creation - project = self.env.ref('sale_timesheet.project_GAP') - task = project.task_ids.filtered(lambda t: t.name == '%s:%s' % (so.name, prod_task.name)) + project = self.project_global + task = project.task_ids.filtered(lambda t: t.name == '%s:%s' % (sale_order.name, self.product_delivery_timesheet2.name)) self.assertTrue(task, 'Sale Service: task is not created') - self.assertEqual(task.partner_id, so.partner_id, 'Sale Service: customer should be the same on task and on SO') + self.assertEqual(task.partner_id, sale_order.partner_id, 'Sale Service: customer should be the same on task and on SO') # register timesheet on task self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': project.id, 'task_id': task.id, 'unit_amount': 50, - 'user_id': self.manager.id, + 'employee_id': self.employee_manager.id, }) - self.assertEqual(so.invoice_status, 'to invoice', 'Sale Service: there should be something to invoice after registering timesheets') - so.action_invoice_create() - line = so.order_line + self.assertEqual(sale_order.invoice_status, 'to invoice', 'Sale Service: there should be sale_ordermething to invoice after registering timesheets') + sale_order.action_invoice_create() + line = sale_order.order_line self.assertTrue(line.product_uom_qty == line.qty_delivered == line.qty_invoiced, 'Sale Service: line should be invoiced completely') - self.assertEqual(so.invoice_status, 'invoiced', 'Sale Service: SO should be invoiced') - self.assertEqual(so.tasks_count, 1, "A task should have been created on SO confirmation.") + self.assertEqual(sale_order.invoice_status, 'invoiced', 'Sale Service: SO should be invoiced') + self.assertEqual(sale_order.tasks_count, 1, "A task should have been created on SO confirmation.") # Add a line on the confirmed SO, and it should generate a new task directly product_service_task = self.env['product.product'].create({ @@ -60,7 +68,49 @@ class TestSaleService(TestSale): 'product_uom_qty': 10, 'product_uom': product_service_task.uom_id.id, 'price_unit': product_service_task.list_price, - 'order_id': so.id, + 'order_id': sale_order.id, }) - self.assertEqual(so.tasks_count, 2, "Adding a new service line on a confirmer SO should create a new task.") + self.assertEqual(sale_order.tasks_count, 2, "Adding a new service line on a confirmer SO should create a new task.") + + def test_timesheet_uom(self): + """ Test timesheet invoicing and uom conversion """ + # create SO and confirm it + uom_days = self.env.ref('product.product_uom_day') + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner_usd.id, + 'partner_invoice_id': self.partner_usd.id, + 'partner_shipping_id': self.partner_usd.id, + 'order_line': [ + (0, 0, { + 'name': self.product_delivery_timesheet3.name, + 'product_id': self.product_delivery_timesheet3.id, + 'product_uom_qty': 5, + 'product_uom': uom_days.id, + 'price_unit': self.product_delivery_timesheet3.list_price + }) + ], + 'pricelist_id': self.pricelist_usd.id, + }) + sale_order.action_confirm() + task = self.env['project.task'].search([('sale_line_id', '=', sale_order.order_line.id)]) + + # let's log some timesheets + self.env['account.analytic.line'].create({ + 'name': 'Test Line', + 'project_id': sale_order.project_project_id.id, + 'task_id': task.id, + 'unit_amount': 16, + 'employee_id': self.employee_manager.id, + }) + self.assertEqual(sale_order.order_line.qty_delivered, 2, 'Sale: uom conversion of timesheets is wrong') + + self.env['account.analytic.line'].create({ + 'name': 'Test Line', + 'project_id': sale_order.project_project_id.id, + 'task_id': task.id, + 'unit_amount': 24, + 'employee_id': self.employee_user.id, + }) + sale_order.action_invoice_create() + self.assertEqual(sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so') diff --git a/addons/sale_timesheet/tests/test_sale_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py index d75991411cb..bb8cf479a1d 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet.py @@ -1,140 +1,271 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tools import float_is_zero +from odoo.tools import float_is_zero, float_compare -from odoo.addons.sale.tests.test_sale_common import TestSale +from odoo.addons.sale_timesheet.tests.common import CommonTest -class TestSaleTimesheet(TestSale): - - def setUp(self): - super(TestSaleTimesheet, self).setUp() - # link hr employee to res.users of sale tests - self.employee_user = self.env['hr.employee'].create({ - 'name': 'Employee User', - 'user_id': self.user.id, - }) - self.employee_manager = self.env['hr.employee'].create({ - 'name': 'Employee Manager', - 'user_id': self.manager.id, - }) +class TestSaleTimesheet(CommonTest): + """ This test suite provide tests for the 3 main flows of selling services: + - Selling services based on ordered quantities + - Selling timesheet based on delivered quantities + - Selling milestones, based on manual delivered quantities + For that, we check the task/project created, the invoiced amounts, the delivered + quantities changes, ... + """ def test_timesheet_order(self): - """ Test timesheet invoicing with 'invoice on order' timetracked products """ - # intial so - prod_ts = self.env.ref('product.service_order_01') - so_vals = { - 'partner_id': self.partner.id, - 'partner_invoice_id': self.partner.id, - 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': prod_ts.name, 'product_id': prod_ts.id, 'product_uom_qty': 50, 'product_uom': prod_ts.uom_id.id, 'price_unit': prod_ts.list_price})], - 'pricelist_id': self.env.ref('product.list0').id, - } - so = self.env['sale.order'].create(so_vals) - so.action_confirm() - so.action_invoice_create() + """ Test timesheet invoicing with 'invoice on order' timetracked products + 1. create SO with 2 ordered product and confirm + 2. create invoice + 3. log timesheet + 4. add new SO line (ordered service) + 5. create new invoice + """ + # create SO and confirm it + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner_usd.id, + 'partner_invoice_id': self.partner_usd.id, + 'partner_shipping_id': self.partner_usd.id, + 'pricelist_id': self.pricelist_usd.id, + }) + so_line_ordered_project_only = self.env['sale.order.line'].create({ + 'name': self.product_order_timesheet4.name, + 'product_id': self.product_order_timesheet4.id, + 'product_uom_qty': 10, + 'product_uom': self.product_order_timesheet4.uom_id.id, + 'price_unit': self.product_order_timesheet4.list_price, + 'order_id': sale_order.id, + }) + so_line_ordered_global_project = self.env['sale.order.line'].create({ + 'name': self.product_order_timesheet2.name, + 'product_id': self.product_order_timesheet2.id, + 'product_uom_qty': 50, + 'product_uom': self.product_order_timesheet2.uom_id.id, + 'price_unit': self.product_order_timesheet2.list_price, + 'order_id': sale_order.id, + }) + so_line_ordered_project_only.product_id_change() + so_line_ordered_global_project.product_id_change() + sale_order.action_confirm() + task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_ordered_global_project.id)]) - # let's log some timesheets + self.assertEqual(sale_order.tasks_count, 1, "One task should have been created on SO confirmation") + self.assertTrue(sale_order.project_project_id, "A project should have been created by the SO, when confirmed.") + + # create invoice + sale_order.action_invoice_create() + + # let's log some timesheets (on the project created by so_line_ordered_project_only) self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, + 'project_id': sale_order.project_project_id.id, + 'task_id': task_serv2.id, 'unit_amount': 10.5, 'employee_id': self.employee_user.id, }) - self.assertEqual(so.order_line.qty_delivered, 10.5, 'Sale Timesheet: timesheet does not increase delivered quantity on so line') - self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so') + self.assertEqual(so_line_ordered_global_project.qty_delivered, 10.5, 'Timesheet directly on project does not increase delivered quantity on so line') + self.assertEqual(sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so') self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, + 'project_id': sale_order.project_project_id.id, + 'task_id': task_serv2.id, 'unit_amount': 39.5, 'employee_id': self.employee_user.id, }) - self.assertEqual(so.order_line.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line') - self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so') + self.assertEqual(so_line_ordered_global_project.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line') + self.assertEqual(sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so') self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, + 'project_id': sale_order.project_project_id.id, 'unit_amount': 10, 'employee_id': self.employee_user.id, }) - self.assertEqual(so.order_line.qty_delivered, 60, 'Sale Timesheet: timesheet does not increase delivered quantity on so line') - self.assertEqual(so.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so') + self.assertEqual(so_line_ordered_project_only.qty_delivered, 0.0, 'Timesheet directly on project does not increase delivered quantity on so line') + + # log timesheet on task in global project (higher than the initial ordrered qty) + self.env['account.analytic.line'].create({ + 'name': 'Test Line', + 'project_id': sale_order.project_project_id.id, + 'task_id': task_serv2.id, + 'unit_amount': 5, + 'employee_id': self.employee_user.id, + }) + self.assertEqual(sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so') + + # add so line with produdct "create task in new project". (project will be the one from SO) + so_line_ordered_task_new_project = self.env['sale.order.line'].create({ + 'name': self.product_order_timesheet3.name, + 'product_id': self.product_order_timesheet3.id, + 'product_uom_qty': 3, + 'product_uom': self.product_order_timesheet3.uom_id.id, + 'price_unit': self.product_order_timesheet3.list_price, + 'order_id': sale_order.id, + }) + task_serv3 = self.env['project.task'].search([('sale_line_id', '=', so_line_ordered_task_new_project.id)]) + self.assertEqual(sale_order.invoice_status, 'to invoice', 'Sale Timesheet: Adding a new service line (so line) should put the SO in "to invocie" state.') + self.assertEqual(sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation") + + # create invoice + invoice_id = sale_order.action_invoice_create() + invoice = self.env['account.invoice'].browse(invoice_id) + self.assertEqual(len(sale_order.invoice_ids), 2, "A second invoice should have been created from the SO") + self.assertTrue(float_is_zero(invoice.amount_total - so_line_ordered_task_new_project.price_unit * 3, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') + self.assertEqual(sale_order.project_project_id, task_serv3.project_id, "When creating task in new project, the task should be in SO project (if already exists), otherwise it created one.") def test_timesheet_delivery(self): - """ Test timesheet invoicing with 'invoice on delivery' timetracked products """ - inv_obj = self.env['account.invoice'] - # intial so - prod_ts = self.env.ref('product.product_product_2') - so_vals = { - 'partner_id': self.partner.id, - 'partner_invoice_id': self.partner.id, - 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': prod_ts.name, 'product_id': prod_ts.id, 'product_uom_qty': 50, 'product_uom': prod_ts.uom_id.id, 'price_unit': prod_ts.list_price})], - 'pricelist_id': self.env.ref('product.list0').id, - } - so = self.env['sale.order'].create(so_vals) - so.action_confirm() - self.assertEqual(so.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation') + """ Test timesheet invoicing with 'invoice on delivery' timetracked products + 1. Create SO and confirm it + 2. log timesheet + 3. create invoice + 4. log other timesheet + 5. create a second invoice + 6. add new SO line (delivered service) + """ + # create SO and confirm it + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner_usd.id, + 'partner_invoice_id': self.partner_usd.id, + 'partner_shipping_id': self.partner_usd.id, + 'pricelist_id': self.pricelist_usd.id, + }) + so_line_deliver_global_project = self.env['sale.order.line'].create({ + 'name': self.product_delivery_timesheet2.name, + 'product_id': self.product_delivery_timesheet2.id, + 'product_uom_qty': 50, + 'product_uom': self.product_delivery_timesheet2.uom_id.id, + 'price_unit': self.product_delivery_timesheet2.list_price, + 'order_id': sale_order.id, + }) + so_line_deliver_task_project = self.env['sale.order.line'].create({ + 'name': self.product_delivery_timesheet3.name, + 'product_id': self.product_delivery_timesheet3.id, + 'product_uom_qty': 20, + 'product_uom': self.product_delivery_timesheet3.uom_id.id, + 'price_unit': self.product_delivery_timesheet3.list_price, + 'order_id': sale_order.id, + }) + so_line_deliver_global_project.product_id_change() + so_line_deliver_task_project.product_id_change() + + # confirm SO + sale_order.action_confirm() + task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_global_project.id)]) + task_serv3 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_task_project.id)]) + + self.assertEqual(task_serv2.project_id, self.project_global, "Sale Timesheet: task should be created in global project") + self.assertTrue(task_serv2, "Sale Timesheet: on SO confirmation, a task should have been created in global project") + self.assertTrue(task_serv3, "Sale Timesheet: on SO confirmation, a task should have been created in a new project") + self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation') + self.assertEqual(sale_order.analytic_account_id, task_serv3.project_id.analytic_account_id, "SO should have create a project") + self.assertEqual(sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation") + # let's log some timesheets self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, + 'project_id': task_serv2.project_id.id, # global project + 'task_id': task_serv2.id, 'unit_amount': 10.5, 'employee_id': self.employee_manager.id, }) - self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged') - inv_id = so.action_invoice_create() - inv = inv_obj.browse(inv_id) - self.assertTrue(float_is_zero(inv.amount_total - so.order_line.price_unit * 10.5, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') + self.assertEqual(so_line_deliver_global_project.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged') + self.assertEqual(so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line') + self.assertEqual(sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged') + # invoice SO + invoice_id = sale_order.action_invoice_create() + invoice = self.env['account.invoice'].browse(invoice_id) + self.assertTrue(float_is_zero(invoice.amount_total - so_line_deliver_global_project.price_unit * 10.5, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') + + # log some timesheets again self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, + 'project_id': task_serv2.project_id.id, # global project + 'task_id': task_serv2.id, 'unit_amount': 39.5, 'employee_id': self.employee_user.id, }) - self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so') - so.action_invoice_create() - self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now') + self.assertEqual(so_line_deliver_global_project.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged') + self.assertEqual(so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line') + self.assertEqual(sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so') + # create a second invoice + sale_order.action_invoice_create() + self.assertEqual(so_line_deliver_global_project.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged') + self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now') + + # add a line on SO + so_line_deliver_only_project = self.env['sale.order.line'].create({ + 'name': self.product_delivery_timesheet4.name, + 'product_id': self.product_delivery_timesheet4.id, + 'product_uom_qty': 5, + 'product_uom': self.product_delivery_timesheet4.uom_id.id, + 'price_unit': self.product_delivery_timesheet4.list_price, + 'order_id': sale_order.id, + }) + self.assertEqual(sale_order.project_project_id, task_serv3.project_id, "SO should not have create a second project, since so line 3 already create one project for the SO") + + # let's log some timesheets on the project self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, - 'unit_amount': 10, + 'project_id': sale_order.project_project_id.id, # global project + 'unit_amount': 7, 'employee_id': self.employee_user.id, }) - self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: supplementary timesheets do not change the status of the SO') + # import pdb;pdb.set_trace() + self.assertTrue(float_is_zero(so_line_deliver_only_project.qty_delivered, precision_digits=2), "Timesheeting on project should not incremented the delivered quantity on the SO line") + self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now') - def test_timesheet_uom(self): - """ Test timesheet invoicing and uom conversion """ - # intial so - prod_ts = self.env.ref('product.product_product_2') - uom_days = self.env.ref('product.product_uom_day') - so_vals = { - 'partner_id': self.partner.id, - 'partner_invoice_id': self.partner.id, - 'partner_shipping_id': self.partner.id, - 'order_line': [(0, 0, {'name': prod_ts.name, 'product_id': prod_ts.id, 'product_uom_qty': 5, 'product_uom': uom_days.id, 'price_unit': prod_ts.list_price})], - 'pricelist_id': self.env.ref('product.list0').id, - } - so = self.env['sale.order'].create(so_vals) - so.action_confirm() - # let's log some timesheets + def test_timesheet_manual(self): + """ Test timesheet invoicing with 'invoice on delivery' timetracked products + """ + # create SO and confirm it + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner_usd.id, + 'partner_invoice_id': self.partner_usd.id, + 'partner_shipping_id': self.partner_usd.id, + 'pricelist_id': self.pricelist_usd.id, + }) + so_line_manual_global_project = self.env['sale.order.line'].create({ + 'name': self.product_delivery_manual2.name, + 'product_id': self.product_delivery_manual2.id, + 'product_uom_qty': 50, + 'product_uom': self.product_delivery_manual2.uom_id.id, + 'price_unit': self.product_delivery_manual2.list_price, + 'order_id': sale_order.id, + }) + so_line_manual_only_project = self.env['sale.order.line'].create({ + 'name': self.product_delivery_manual4.name, + 'product_id': self.product_delivery_manual4.id, + 'product_uom_qty': 20, + 'product_uom': self.product_delivery_manual4.uom_id.id, + 'price_unit': self.product_delivery_manual4.list_price, + 'order_id': sale_order.id, + }) + + # confirm SO + sale_order.action_confirm() + self.assertTrue(sale_order.project_project_id, "Sales Order should have create a project") + self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: manually product should not need to be invoiced on so confirmation') + + # let's log some timesheets (on task and project) self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, - 'unit_amount': 16, + 'project_id': self.project_global.id, # global project + 'task_id': so_line_manual_global_project.task_id.id, + 'unit_amount': 6, 'employee_id': self.employee_manager.id, }) - self.assertEqual(so.order_line.qty_delivered, 2, 'Sale: uom conversion of timesheets is wrong') self.env['account.analytic.line'].create({ 'name': 'Test Line', - 'project_id': so.project_project_id.id, - 'unit_amount': 24, - 'employee_id': self.employee_user.id, + 'project_id': sale_order.project_project_id.id, # global project + 'unit_amount': 3, + 'employee_id': self.employee_manager.id, }) - so.action_invoice_create() - self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so') + + self.assertEqual(so_line_manual_global_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created task.") + self.assertEqual(so_line_manual_only_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created project.") + self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation') diff --git a/addons/sale_timesheet/tests/test_timesheet_revenue.py b/addons/sale_timesheet/tests/test_timesheet_revenue.py index 4e72ba3c9a2..79c14e68c16 100644 --- a/addons/sale_timesheet/tests/test_timesheet_revenue.py +++ b/addons/sale_timesheet/tests/test_timesheet_revenue.py @@ -35,7 +35,7 @@ class TestSaleTimesheet(TestSale): 'default_code': 'SERV-DELI', 'service_type': 'timesheet', 'service_tracking': 'task_global_project', - 'project_id': self.project.id + 'project_id': self.project.id, }) self.product_order = self.env['product.product'].create({ @@ -49,7 +49,7 @@ class TestSaleTimesheet(TestSale): 'default_code': 'SERV-ORDER', 'service_type': 'timesheet', 'service_tracking': 'task_global_project', - 'project_id': self.project.id + 'project_id': self.project.id, }) # pricelists @@ -65,7 +65,6 @@ class TestSaleTimesheet(TestSale): 'currency_id': self.env.ref('base.EUR').id, 'company_id': self.env.user.company_id.id, }) - # partners self.partner_usd = self.env['res.partner'].create({ 'name': 'Cool Partner in USD',