diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py index c18361894ff..1c072a7a43e 100644 --- a/addons/sale_timesheet/tests/__init__.py +++ b/addons/sale_timesheet/tests/__init__.py @@ -5,3 +5,4 @@ from . import common from . import test_sale_timesheet from . import test_sale_service from . import test_timesheet_revenue +from . import test_reinvoice diff --git a/addons/sale_timesheet/tests/common.py b/addons/sale_timesheet/tests/common.py index 3c13fa71216..1cb86b25bc3 100644 --- a/addons/sale_timesheet/tests/common.py +++ b/addons/sale_timesheet/tests/common.py @@ -1,42 +1,41 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import common +from odoo.addons.sale.tests.test_sale_common import TestCommonSaleNoChart -class CommonTest(common.SavepointCase): +class TestCommonSaleTimesheetNoChart(TestCommonSaleNoChart): @classmethod - def setUpClass(cls): - super(CommonTest, cls).setUpClass() + def setUpEmployees(cls): + # 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, + }) - # 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, - }) + @classmethod + def setUpServiceProducts(cls): + """ Create Service product for all kind, with each tracking policy. """ + # Account and project cls.account_sale = cls.env['account.account'].create({ - 'code': 'X2020', + 'code': 'SERV-2020', '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 + uom_hour = cls.env.ref('product.product_uom_hour') # -- ordered quantities (ordered, timesheet) cls.product_order_timesheet1 = cls.env['product.product'].create({ @@ -45,8 +44,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-ORDERED1', 'service_type': 'timesheet', 'service_tracking': 'no', @@ -60,8 +59,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-ORDERED2', 'service_type': 'timesheet', 'service_tracking': 'task_global_project', @@ -75,8 +74,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-ORDERED3', 'service_type': 'timesheet', 'service_tracking': 'task_new_project', @@ -90,8 +89,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-ORDERED4', 'service_type': 'timesheet', 'service_tracking': 'project_only', @@ -107,8 +106,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI1', 'service_type': 'timesheet', 'service_tracking': 'no', @@ -122,8 +121,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI2', 'service_type': 'timesheet', 'service_tracking': 'task_global_project', @@ -137,8 +136,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI3', 'service_type': 'timesheet', 'service_tracking': 'task_new_project', @@ -152,8 +151,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI4', 'service_type': 'timesheet', 'service_tracking': 'project_only', @@ -162,15 +161,15 @@ class CommonTest(common.SavepointCase): 'property_account_income_id': cls.account_sale.id, }) - # -- milestons (delivered, manual) + # -- milestones (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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI1', 'service_type': 'manual', 'service_tracking': 'no', @@ -184,8 +183,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI2', 'service_type': 'manual', 'service_tracking': 'task_global_project', @@ -199,8 +198,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI3', 'service_type': 'manual', 'service_tracking': 'task_new_project', @@ -214,8 +213,8 @@ class CommonTest(common.SavepointCase): '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, + 'uom_id': uom_hour.id, + 'uom_po_id': uom_hour.id, 'default_code': 'SERV-DELI4', 'service_type': 'manual', 'service_tracking': 'project_only', @@ -223,58 +222,3 @@ class CommonTest(common.SavepointCase): '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.bank_journal_euro = cls.env['account.journal'].create({ - 'name': 'Sale Journal - Test', - 'type': 'sale', - 'code': 'SJT', - 'currency_id': cls.env.ref('base.EUR').id, - }) - - cls.bank_journal_usd = cls.env['account.journal'].create({ - 'name': 'Sale Journal - Test US', - 'type': 'sale', - 'code': 'SJTU', - 'currency_id': cls.env.ref('base.USD').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_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py new file mode 100644 index 00000000000..1ccf022eb13 --- /dev/null +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -0,0 +1,337 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheetNoChart + + +class TestReInvoice(TestCommonSaleTimesheetNoChart): + + @classmethod + def setUpClass(cls): + super(TestReInvoice, cls).setUpClass() + + cls.setUpEmployees() + cls.setUpExpenseProducts() + cls.setUpAdditionalAccounts() + cls.setUpAccountJournal() + + # patch expense products to make them services creating task/project + service_values = { + 'type': 'service', + 'service_type': 'timesheet', + 'service_tracking': 'task_new_project' + } + cls.product_ordered_cost.write(service_values) + cls.product_deliver_cost.write(service_values) + cls.product_order_sales_price.write(service_values) + cls.product_deliver_sales_price.write(service_values) + cls.product_no_expense.write(service_values) + + # create AA, SO and invoices + cls.analytic_account = cls.env['account.analytic.account'].create({ + 'name': 'Test AA', + 'code': 'TESTSALE_TIMESHEET_REINVOICE', + 'company_id': cls.partner_customer_usd.company_id.id, + 'partner_id': cls.partner_customer_usd.id + }) + + cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({ + 'partner_id': cls.partner_customer_usd.id, + 'partner_invoice_id': cls.partner_customer_usd.id, + 'partner_shipping_id': cls.partner_customer_usd.id, + 'analytic_account_id': cls.analytic_account.id, + }) + + Invoice = cls.env['account.invoice'].with_context(mail_notrack=True, mail_create_nolog=True) + cls.invoiceA = Invoice.create({ + 'name': 'Test Invoice', + 'type': 'in_invoice', + 'partner_id': cls.partner_customer_usd.id, + 'account_id': cls.account_payable.id, + 'journal_id': cls.journal_purchase.id, + 'currency_id': cls.env.user.company_id.currency_id.id, + }) + cls.invoiceB = Invoice.create({ + 'name': 'Test Invoice 2', + 'type': 'in_invoice', + 'partner_id': cls.partner_customer_usd.id, + 'account_id': cls.account_payable.id, + 'journal_id': cls.journal_purchase.id, + 'currency_id': cls.env.user.company_id.currency_id.id, + }) + + def test_at_cost(self): + """ Test vendor bill at cost for product based on ordered and delivered quantities. """ + # create SO line and confirm SO (with only one line) + sale_order_line1 = self.env['sale.order.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'product_uom_qty': 2, + 'product_uom': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line1.product_id_change() + sale_order_line2 = self.env['sale.order.line'].create({ + 'name': self.product_deliver_cost.name, + 'product_id': self.product_deliver_cost.id, + 'product_uom_qty': 4, + 'product_uom': self.product_deliver_cost.uom_id.id, + 'price_unit': self.product_deliver_cost.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line2.product_id_change() + + self.sale_order.onchange_partner_id() + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + self.assertEquals(sale_order_line1.qty_delivered_method, 'timesheet', "Delivered quantity of 'service' SO line should be computed by timesheet amount") + self.assertEquals(sale_order_line2.qty_delivered_method, 'timesheet', "Delivered quantity of 'service' SO line should be computed by timesheet amount") + + # let's log some timesheets (on the project created by sale_order_line1) + task_sol1 = sale_order_line1.task_id + self.env['account.analytic.line'].create({ + 'name': 'Test Line', + 'project_id': task_sol1.project_id.id, + 'task_id': task_sol1.id, + 'unit_amount': 1, + 'employee_id': self.employee_user.id, + }) + + # create invoice lines and validate it + invoice_lineA1 = self.env['account.invoice.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'quantity': 3, + 'uom_id': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + invoice_lineA2 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_cost.name, + 'product_id': self.product_deliver_cost.id, + 'quantity': 3, + 'uom_id': self.product_deliver_cost.uom_id.id, + 'price_unit': self.product_deliver_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + self.invoiceA.action_invoice_open() + + sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.product_ordered_cost) + sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.product_deliver_cost) + + self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product") + self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product") + self.assertEquals(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)") + + self.assertEquals(sale_order_line1.qty_delivered, 1, "Exising SO line 1 should not be impacted by reinvoicing product at cost") + self.assertEquals(sale_order_line2.qty_delivered, 0, "Exising SO line 2 should not be impacted by reinvoicing product at cost") + + self.assertFalse(sale_order_line3.task_id, "Adding a new expense SO line should not create a task (sol3)") + self.assertFalse(sale_order_line4.task_id, "Adding a new expense SO line should not create a task (sol4)") + self.assertEquals(len(self.sale_order.project_ids), 1, "SO create only one project with its service line. Adding new expense SO line should not impact that") + + self.assertEquals((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.product_ordered_cost.standard_price, invoice_lineA1.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + self.assertEquals((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.product_deliver_cost.standard_price, invoice_lineA2.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + + self.assertEquals(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount") + self.assertEquals(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount") + + # create second invoice lines and validate it + invoice_lineB1 = self.env['account.invoice.line'].create({ + 'name': self.product_ordered_cost.name, + 'product_id': self.product_ordered_cost.id, + 'quantity': 2, + 'uom_id': self.product_ordered_cost.uom_id.id, + 'price_unit': self.product_ordered_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + invoice_lineB2 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_cost.name, + 'product_id': self.product_deliver_cost.id, + 'quantity': 2, + 'uom_id': self.product_deliver_cost.uom_id.id, + 'price_unit': self.product_deliver_cost.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + self.invoiceB.action_invoice_open() + + sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.product_ordered_cost) + sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.product_deliver_cost) + + self.assertTrue(sale_order_line5, "A new sale line should have been created with ordered product") + self.assertTrue(sale_order_line6, "A new sale line should have been created with delivered product") + + self.assertEquals(len(self.sale_order.order_line), 6, "There should be still 4 lines on the SO, no new created") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 4, "There should be still 2 expenses lines on the SO") + + self.assertEquals((sale_order_line5.price_unit, sale_order_line5.qty_delivered, sale_order_line5.product_uom_qty, sale_order_line5.qty_invoiced), (self.product_ordered_cost.standard_price, invoice_lineB1.quantity, 0, 0), 'Sale line 5 is wrong after confirming 2e vendor invoice') + self.assertEquals((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line6.product_uom_qty, sale_order_line6.qty_invoiced), (self.product_deliver_cost.standard_price, invoice_lineB2.quantity, 0, 0), 'Sale line 6 is wrong after confirming 2e vendor invoice') + + def test_sales_price(self): + """ Test invoicing vendor bill at sales price for products based on delivered and ordered quantities. Check no existing SO line is incremented, but when invoicing a + second time, increment only the delivered so line. + """ + # create SO line and confirm SO (with only one line) + sale_order_line1 = self.env['sale.order.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'product_uom_qty': 2, + 'qty_delivered': 1, + 'product_uom': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line1.product_id_change() + sale_order_line2 = self.env['sale.order.line'].create({ + 'name': self.product_order_sales_price.name, + 'product_id': self.product_order_sales_price.id, + 'product_uom_qty': 3, + 'qty_delivered': 1, + 'product_uom': self.product_order_sales_price.uom_id.id, + 'price_unit': self.product_order_sales_price.list_price, + 'order_id': self.sale_order.id, + }) + sale_order_line2.product_id_change() + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + # let's log some timesheets (on the project created by sale_order_line1) + task_sol1 = sale_order_line1.task_id + self.env['account.analytic.line'].create({ + 'name': 'Test Line', + 'project_id': task_sol1.project_id.id, + 'task_id': task_sol1.id, + 'unit_amount': 1, + 'employee_id': self.employee_user.id, + }) + + # create invoice lines and validate it + invoice_lineA1 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'quantity': 3, + 'uom_id': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + invoice_lineA2 = self.env['account.invoice.line'].create({ + 'name': self.product_order_sales_price.name, + 'product_id': self.product_order_sales_price.id, + 'quantity': 3, + 'uom_id': self.product_order_sales_price.uom_id.id, + 'price_unit': self.product_order_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + self.invoiceA.action_invoice_open() + + sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.product_deliver_sales_price) + sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.product_order_sales_price) + + self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product") + self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product") + self.assertEquals(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)") + + self.assertEquals(sale_order_line1.qty_delivered, 1, "Exising SO line 1 should not be impacted by reinvoicing product at cost") + self.assertEquals(sale_order_line2.qty_delivered, 0, "Exising SO line 2 should not be impacted by reinvoicing product at cost") + + self.assertFalse(sale_order_line3.task_id, "Adding a new expense SO line should not create a task (sol3)") + self.assertFalse(sale_order_line4.task_id, "Adding a new expense SO line should not create a task (sol4)") + self.assertEquals(len(self.sale_order.project_ids), 1, "SO create only one project with its service line. Adding new expense SO line should not impact that") + + self.assertEquals((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.product_deliver_sales_price.list_price, invoice_lineA1.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + self.assertEquals((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.product_order_sales_price.list_price, invoice_lineA2.quantity, 0, 0), 'Sale line is wrong after confirming vendor invoice') + + self.assertEquals(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 3 should be computed by analytic amount") + self.assertEquals(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 4 should be computed by analytic amount") + + # create second invoice lines and validate it + invoice_lineB1 = self.env['account.invoice.line'].create({ + 'name': self.product_deliver_sales_price.name, + 'product_id': self.product_deliver_sales_price.id, + 'quantity': 2, + 'uom_id': self.product_deliver_sales_price.uom_id.id, + 'price_unit': self.product_deliver_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + invoice_lineB2 = self.env['account.invoice.line'].create({ + 'name': self.product_order_sales_price.name, + 'product_id': self.product_order_sales_price.id, + 'quantity': 2, + 'uom_id': self.product_order_sales_price.uom_id.id, + 'price_unit': self.product_order_sales_price.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceB.id, + }) + self.invoiceB.action_invoice_open() + + sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.product_deliver_sales_price) + sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.product_order_sales_price) + + self.assertFalse(sale_order_line5, "No new sale line should have been created with delivered product !!") + self.assertTrue(sale_order_line6, "A new sale line should have been created with ordered product") + + self.assertEquals(len(self.sale_order.order_line), 5, "There should be 5 lines on the SO, 1 new created and 1 incremented") + self.assertEquals(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 3, "There should be 3 expenses lines on the SO") + + self.assertEquals((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line6.qty_invoiced), (self.product_order_sales_price.list_price, invoice_lineB2.quantity, 0, 0), 'Sale line is wrong after confirming 2e vendor invoice') + + def test_no_expense(self): + """ Test invoicing vendor bill with no policy. Check nothing happen. """ + # confirm SO + sale_order_line = self.env['sale.order.line'].create({ + 'name': self.product_no_expense.name, + 'product_id': self.product_no_expense.id, + 'product_uom_qty': 2, + 'qty_delivered': 1, + 'product_uom': self.product_no_expense.uom_id.id, + 'price_unit': self.product_no_expense.list_price, + 'order_id': self.sale_order.id, + }) + self.sale_order._compute_tax_id() + self.sale_order.action_confirm() + + # create invoice lines and validate it + invoice_lineA1 = self.env['account.invoice.line'].create({ + 'name': self.product_no_expense.name, + 'product_id': self.product_no_expense.id, + 'quantity': 3, + 'uom_id': self.product_no_expense.uom_id.id, + 'price_unit': self.product_no_expense.standard_price, + 'account_analytic_id': self.analytic_account.id, + 'account_id': self.account_income.id, + 'invoice_id': self.invoiceA.id, + }) + self.invoiceA.action_invoice_open() + + # let's log some timesheets (on the project created by sale_order_line1) + task_sol1 = sale_order_line.task_id + self.env['account.analytic.line'].create({ + 'name': 'Test Line', + 'project_id': task_sol1.project_id.id, + 'task_id': task_sol1.id, + 'unit_amount': 1, + 'employee_id': self.employee_user.id, + }) + + self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) when validating vendor bill") + self.assertEquals(sale_order_line.qty_delivered, 1, "The delivered quantity of SO line should not have been incremented") + self.assertTrue(invoice_lineA1.invoice_id.move_id.mapped('line_ids.analytic_line_ids'), "Analytic lines should be generated") diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index 75c954cdebf..db1046bf4a6 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -1,43 +1,53 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.sale_timesheet.tests.common import CommonTest +from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheetNoChart from odoo.exceptions import UserError -class TestSaleService(CommonTest): +class TestSaleService(TestCommonSaleTimesheetNoChart): """ This test suite provide checks for miscellaneous small things. """ + @classmethod + def setUpClass(cls): + super(TestSaleService, cls).setUpClass() + # set up + cls.setUpEmployees() + cls.setUpServiceProducts() + + cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({ + 'partner_id': cls.partner_customer_usd.id, + 'partner_invoice_id': cls.partner_customer_usd.id, + 'partner_shipping_id': cls.partner_customer_usd.id, + }) + def test_sale_service(self): """ 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, - } - sale_order = self.env['sale.order'].create(sale_order_vals) - sale_order.order_line._compute_product_updatable() - self.assertTrue(sale_order.order_line[0].product_updatable) - sale_order.action_confirm() - sale_order.order_line._compute_product_updatable() - self.assertFalse(sale_order.order_line[0].product_updatable) - self.assertEqual(sale_order.invoice_status, 'no', 'Sale Service: there should be nothing to invoice after validation') + sale_order_line = self.env['sale.order.line'].create({ + 'order_id': self.sale_order.id, + '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 + }) + + self.sale_order.order_line._compute_product_updatable() + self.assertTrue(sale_order_line.product_updatable) + self.sale_order.action_confirm() + self.sale_order.order_line._compute_product_updatable() + + self.assertFalse(sale_order_line.product_updatable) + self.assertEqual(self.sale_order.invoice_status, 'no', 'Sale Service: there should be nothing to invoice after validation') # check task creation project = self.project_global - task = project.task_ids.filtered(lambda t: t.name == '%s:%s' % (sale_order.name, self.product_delivery_timesheet2.name)) + task = project.task_ids.filtered(lambda t: t.name == '%s:%s' % (self.sale_order.name, self.product_delivery_timesheet2.name)) self.assertTrue(task, 'Sale Service: task is not created') - self.assertEqual(task.partner_id, sale_order.partner_id, 'Sale Service: customer should be the same on task and on SO') - self.assertEqual(task.email_from, sale_order.partner_id.email, 'Sale Service: Task Email should be the same as the SO customer Email') - # register timesheet on task + self.assertEqual(task.partner_id, self.sale_order.partner_id, 'Sale Service: customer should be the same on task and on SO') + self.assertEqual(task.email_from, self.sale_order.partner_id.email, 'Sale Service: Task Email should be the same as the SO customer Email') + + # log timesheet on task self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': project.id, @@ -45,12 +55,12 @@ class TestSaleService(CommonTest): 'unit_amount': 50, 'employee_id': self.employee_manager.id, }) - 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(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.") + self.assertEqual(self.sale_order.invoice_status, 'to invoice', 'Sale Service: there should be sale_ordermething to invoice after registering timesheets') + self.sale_order.action_invoice_create() + + self.assertTrue(sale_order_line.product_uom_qty == sale_order_line.qty_delivered == sale_order_line.qty_invoiced, 'Sale Service: line should be invoiced completely') + self.assertEqual(self.sale_order.invoice_status, 'invoiced', 'Sale Service: SO should be invoiced') + self.assertEqual(self.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({ @@ -73,71 +83,58 @@ class TestSaleService(CommonTest): 'product_uom_qty': 10, 'product_uom': product_service_task.uom_id.id, 'price_unit': product_service_task.list_price, - 'order_id': sale_order.id, + 'order_id': self.sale_order.id, }) - self.assertEqual(sale_order.tasks_count, 2, "Adding a new service line on a confirmer SO should create a new task.") + self.assertEqual(self.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_line = self.env['sale.order.line'].create({ + 'order_id': self.sale_order.id, + '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 }) - sale_order.action_confirm() - task = self.env['project.task'].search([('sale_line_id', '=', sale_order.order_line.id)]) + self.sale_order.action_confirm() + task = self.env['project.task'].search([('sale_line_id', '=', sale_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, + 'project_id': self.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.assertEqual(self.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, + 'project_id': self.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') + self.sale_order.action_invoice_create() + self.assertEqual(self.sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so') def test_task_so_line_assignation(self): # 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': 10, 'product_uom': self.product_delivery_timesheet2.uom_id.id, 'price_unit': self.product_delivery_timesheet2.list_price, - 'order_id': sale_order.id, + 'order_id': self.sale_order.id, }) so_line_deliver_global_project.product_id_change() - sale_order.action_confirm() + self.sale_order.action_confirm() task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_global_project.id)]) # let's log some timesheets (on the project created by so_line_ordered_project_only) @@ -168,7 +165,7 @@ class TestSaleService(CommonTest): self.assertTrue(all([billing_type == 'billable_time' for billing_type in timesheets.mapped('timesheet_invoice_type')]), "Timesheet to a billable time task should be billable") # invoice SO, and validate invoice - invoice_id = sale_order.action_invoice_create()[0] + invoice_id = self.sale_order.action_invoice_create()[0] invoice = self.env['account.invoice'].browse(invoice_id) invoice.action_invoice_open() @@ -179,9 +176,9 @@ class TestSaleService(CommonTest): def test_delivered_quantity(self): # create SO and confirm it sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({ - 'partner_id': self.partner_usd.id, - 'partner_invoice_id': self.partner_usd.id, - 'partner_shipping_id': self.partner_usd.id, + 'partner_id': self.partner_customer_usd.id, + 'partner_invoice_id': self.partner_customer_usd.id, + 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_deliver_new_task_project = self.env['sale.order.line'].create({ diff --git a/addons/sale_timesheet/tests/test_sale_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py index fa4b8d905fa..e48849e078c 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tools import float_is_zero, float_compare +from odoo.tools import float_is_zero -from odoo.addons.sale_timesheet.tests.common import CommonTest +from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheetNoChart -class TestSaleTimesheet(CommonTest): +class TestSaleTimesheet(TestCommonSaleTimesheetNoChart): """ 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 @@ -14,6 +14,13 @@ class TestSaleTimesheet(CommonTest): quantities changes, ... """ + @classmethod + def setUpClass(cls): + super(TestSaleTimesheet, cls).setUpClass() + # set up + cls.setUpEmployees() + cls.setUpServiceProducts() + def test_timesheet_order(self): """ Test timesheet invoicing with 'invoice on order' timetracked products 1. create SO with 2 ordered product and confirm @@ -24,9 +31,9 @@ class TestSaleTimesheet(CommonTest): """ # 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, + 'partner_id': self.partner_customer_usd.id, + 'partner_invoice_id': self.partner_customer_usd.id, + 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_ordered_project_only = self.env['sale.order.line'].create({ @@ -126,9 +133,9 @@ class TestSaleTimesheet(CommonTest): """ # 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, + 'partner_id': self.partner_customer_usd.id, + 'partner_invoice_id': self.partner_customer_usd.id, + 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_deliver_global_project = self.env['sale.order.line'].create({ @@ -222,9 +229,9 @@ class TestSaleTimesheet(CommonTest): """ # 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, + 'partner_id': self.partner_customer_usd.id, + 'partner_invoice_id': self.partner_customer_usd.id, + 'partner_shipping_id': self.partner_customer_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_manual_global_project = self.env['sale.order.line'].create({