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',