sale_timesheet/tests/test_sale_service.py

884 lines
46 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
from odoo.exceptions import UserError, ValidationError
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestSaleService(TestCommonSaleTimesheet):
""" This test suite provide checks for miscellaneous small things. """
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
})
def test_sale_service(self):
""" Test task creation when confirming a sale_order with the corresponding product """
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,
})
self.assertTrue(sale_order_line.product_updatable)
self.sale_order.action_confirm()
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' % (self.sale_order.name, self.product_delivery_timesheet2.name))
self.assertTrue(task, 'Sale Service: task is not created, or it badly named')
self.assertEqual(task.partner_id, self.sale_order.partner_id, 'Sale Service: customer should be the same on task and on SO')
# log timesheet on task
self.env['account.analytic.line'].create({
'name': 'Test Line',
'project_id': project.id,
'task_id': task.id,
'unit_amount': 50,
'employee_id': self.employee_manager.id,
})
self.assertEqual(self.sale_order.invoice_status, 'to invoice', 'Sale Service: there should be sale_ordermething to invoice after registering timesheets')
self.sale_order._create_invoices()
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({
'name': "Delivered Service",
'standard_price': 30,
'list_price': 90,
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': self.env.ref('uom.product_uom_hour').id,
'uom_po_id': self.env.ref('uom.product_uom_hour').id,
'default_code': 'SERV-DELI',
'service_type': 'timesheet',
'service_tracking': 'task_global_project',
'project_id': project.id
})
self.env['sale.order.line'].create({
'product_id': product_service_task.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
self.assertEqual(self.sale_order.tasks_count, 2, "Adding a new service line on a confirmer SO should create a new task.")
# delete timesheets before deleting the task, so as to trigger the error
# about linked sales order lines and not the one about linked timesheets
task.timesheet_ids.unlink()
# unlink automatically task from the SOL when deleting the task
task.unlink()
self.assertFalse(sale_order_line.task_id, "Deleting the task its should automatically unlink the task from SOL.")
def test_timesheet_uom(self):
""" Test timesheet invoicing and uom conversion """
# create SO and confirm it
uom_days = self.env.ref('uom.product_uom_day')
sale_order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': uom_days.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': task.project_id.id,
'task_id': task.id,
'unit_amount': 16,
'employee_id': self.employee_manager.id,
})
self.assertEqual(sale_order_line.qty_delivered, 2, 'Sale: uom conversion of timesheets is wrong')
self.env['account.analytic.line'].create({
'name': 'Test Line',
'project_id': task.project_id.id,
'task_id': task.id,
'unit_amount': 24,
'employee_id': self.employee_user.id,
})
self.sale_order._create_invoices()
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 line and confirm it
so_line_deliver_global_project = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
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)
timesheets = self.env['account.analytic.line']
timesheets |= self.env['account.analytic.line'].create({
'name': 'Test Line',
'project_id': task_serv2.project_id.id,
'task_id': task_serv2.id,
'unit_amount': 4,
'employee_id': self.employee_user.id,
})
timesheets |= self.env['account.analytic.line'].create({
'name': 'Test Line',
'project_id': task_serv2.project_id.id,
'task_id': task_serv2.id,
'unit_amount': 1,
'employee_id': self.employee_manager.id,
})
self.assertTrue(all([billing_type == 'billable_time' for billing_type in timesheets.mapped('timesheet_invoice_type')]), "All timesheets linked to the task should be on 'billable time'")
self.assertEqual(so_line_deliver_global_project.qty_to_invoice, 5, "Quantity to invoice should have been increased when logging timesheet on delivered quantities task")
# invoice SO, and validate invoice
invoice = self.sale_order._create_invoices()[0]
invoice.action_post()
# make task non billable
task_serv2.write({'sale_line_id': False})
self.assertTrue(all([billing_type == 'billable_time' for billing_type in timesheets.mapped('timesheet_invoice_type')]), "billable type of timesheet should not change when tranfering task into another project")
self.assertEqual(task_serv2.timesheet_ids.mapped('so_line'), so_line_deliver_global_project, "Old invoiced timesheet are not modified when changing the task SO line")
# try to update timesheets, catch error 'You cannot modify invoiced timesheet'
with self.assertRaises(UserError):
timesheets.write({'so_line': False})
def test_delivered_quantity(self):
# create SO line and confirm it
so_line_deliver_new_task_project = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_new_task_project.id)])
# add a timesheet
timesheet1 = self.env['account.analytic.line'].create({
'name': 'Test Line',
'project_id': task_serv2.project_id.id,
'task_id': task_serv2.id,
'unit_amount': 4,
'employee_id': self.employee_user.id,
})
self.assertEqual(so_line_deliver_new_task_project.qty_delivered, timesheet1.unit_amount, 'Delivered quantity should be the same then its only related timesheet.')
# remove the only timesheet
timesheet1.unlink()
self.assertEqual(so_line_deliver_new_task_project.qty_delivered, 0.0, 'Delivered quantity should be reset to zero, since there is no more timesheet.')
# log 2 new timesheets
timesheet2 = self.env['account.analytic.line'].create({
'name': 'Test Line 2',
'project_id': task_serv2.project_id.id,
'task_id': task_serv2.id,
'unit_amount': 4,
'employee_id': self.employee_user.id,
})
timesheet3 = self.env['account.analytic.line'].create({
'name': 'Test Line 3',
'project_id': task_serv2.project_id.id,
'task_id': task_serv2.id,
'unit_amount': 2,
'employee_id': self.employee_user.id,
})
self.assertEqual(so_line_deliver_new_task_project.qty_delivered, timesheet2.unit_amount + timesheet3.unit_amount, 'Delivered quantity should be the sum of the 2 timesheets unit amounts.')
# remove timesheet2
timesheet2.unlink()
self.assertEqual(so_line_deliver_new_task_project.qty_delivered, timesheet3.unit_amount, 'Delivered quantity should be reset to the sum of remaining timesheets unit amounts.')
def test_sale_create_task(self):
""" Check that confirming SO create correctly a task, and reconfirming it does not create a second one. Also check changing
the ordered quantity of a SO line that have created a task should update the planned hours of this task.
"""
so_line1 = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 7,
'order_id': self.sale_order.id,
})
# confirm SO
self.sale_order.action_confirm()
self.assertTrue(so_line1.task_id, "SO confirmation should create a task and link it to SOL")
self.assertTrue(so_line1.project_id, "SO confirmation should create a project and link it to SOL")
self.assertEqual(self.sale_order.tasks_count, 1, "The SO should have only one task")
self.assertEqual(so_line1.task_id.sale_line_id, so_line1, "The created task is also linked to its origin sale line, for invoicing purpose.")
self.assertFalse(so_line1.task_id.user_ids, "The created task should be unassigned")
self.assertEqual(so_line1.product_uom_qty, so_line1.task_id.allocated_hours, "The planned hours should be the same as the ordered quantity of the native SO line")
so_line1.write({'product_uom_qty': 20})
self.assertEqual(so_line1.product_uom_qty, so_line1.task_id.allocated_hours, "The planned hours should have changed when updating the ordered quantity of the native SO line")
# cancel SO
self.sale_order._action_cancel()
self.assertTrue(so_line1.task_id, "SO cancellation should keep the task")
self.assertTrue(so_line1.project_id, "SO cancellation should create a project")
self.assertEqual(self.sale_order.tasks_count, 1, "The SO should still have only one task")
self.assertEqual(so_line1.task_id.sale_line_id, so_line1, "The created task is also linked to its origin sale line, for invoicing purpose.")
so_line1.write({'product_uom_qty': 30})
self.assertEqual(so_line1.product_uom_qty, so_line1.task_id.allocated_hours, "The planned hours should have changed when updating the ordered quantity, even after SO cancellation")
# reconfirm SO
self.sale_order.action_draft()
self.sale_order.action_confirm()
self.assertTrue(so_line1.task_id, "SO reconfirmation should not have create another task")
self.assertTrue(so_line1.project_id, "SO reconfirmation should bit have create another project")
self.assertEqual(self.sale_order.tasks_count, 1, "The SO should still have only one task")
self.assertEqual(so_line1.task_id.sale_line_id, so_line1, "The created task is also linked to its origin sale line, for invoicing purpose.")
self.sale_order.action_lock()
with self.assertRaises(UserError):
so_line1.write({'product_uom_qty': 20})
def test_sale_create_project(self):
""" A SO with multiple product that should create project (with and without template) like ;
Line 1 : Service 1 create project with Template A ===> project created with template A
Line 2 : Service 2 create project no template ==> empty project created
Line 3 : Service 3 create project with Template A ===> Don't create any project because line 1 has already created a project with template A
Line 4 : Service 4 create project no template ==> Don't create any project because line 2 has already created an empty project
Line 5 : Service 5 create project with Template B ===> project created with template B
"""
# second project template and its associated product
project_template2 = self.env['project.project'].create({
'name': 'Second Project TEMPLATE for services',
'allow_timesheets': True,
'active': False, # this template is archived
})
Stage = self.env['project.task.type'].with_context(default_project_id=project_template2.id)
stage1_tmpl2 = Stage.create({
'name': 'Stage 1',
'sequence': 1
})
stage2_tmpl2 = Stage.create({
'name': 'Stage 2',
'sequence': 2
})
product_deli_ts_tmpl = self.env['product.product'].create({
'name': "Service delivered, create project only based on template B",
'standard_price': 17,
'list_price': 34,
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': self.env.ref('uom.product_uom_hour').id,
'uom_po_id': self.env.ref('uom.product_uom_hour').id,
'default_code': 'SERV-DELI4',
'service_type': 'timesheet',
'service_tracking': 'project_only',
'project_template_id': project_template2.id,
'project_id': False,
'taxes_id': False,
'property_account_income_id': self.account_sale.id,
})
# create 5 so lines
so_line1 = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet5.id,
'product_uom_qty': 11,
'order_id': self.sale_order.id,
})
so_line2 = self.env['sale.order.line'].create({
'product_id': self.product_order_timesheet4.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
so_line3 = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet5.id,
'product_uom_qty': 5,
'order_id': self.sale_order.id,
})
so_line4 = self.env['sale.order.line'].create({
'product_id': self.product_delivery_manual3.id,
'product_uom_qty': 4,
'order_id': self.sale_order.id,
})
so_line5 = self.env['sale.order.line'].create({
'product_id': product_deli_ts_tmpl.id,
'product_uom_qty': 8,
'order_id': self.sale_order.id,
})
# confirm SO
self.sale_order.action_confirm()
# check each line has or no generate something
self.assertTrue(so_line1.project_id, "Line1 should have create a project based on template A")
self.assertTrue(so_line2.project_id, "Line2 should have create an empty project")
self.assertEqual(so_line3.project_id, so_line1.project_id, "Line3 should reuse project of line1")
self.assertEqual(so_line4.project_id, so_line2.project_id, "Line4 should reuse project of line2")
self.assertTrue(so_line4.task_id, "Line4 should have create a new task, even if no project created.")
self.assertTrue(so_line5.project_id, "Line5 should have create a project based on template B")
# check all generated project should be active, even if the template is not
self.assertTrue(so_line1.project_id.active, "Project of Line1 should be active")
self.assertTrue(so_line2.project_id.active, "Project of Line2 should be active")
self.assertTrue(so_line5.project_id.active, "Project of Line5 should be active")
# check generated stuff are correct
self.assertTrue(so_line1.project_id in self.project_template_state.project_ids, "Stage 1 from template B should be part of project from so line 1")
self.assertTrue(so_line1.project_id in self.project_template_state.project_ids, "Stage 1 from template B should be part of project from so line 1")
self.assertTrue(so_line5.project_id in stage1_tmpl2.project_ids, "Stage 1 from template B should be part of project from so line 5")
self.assertTrue(so_line5.project_id in stage2_tmpl2.project_ids, "Stage 2 from template B should be part of project from so line 5")
self.assertTrue(so_line1.project_id.allow_timesheets, "Create project should allow timesheets")
self.assertTrue(so_line2.project_id.allow_timesheets, "Create project should allow timesheets")
self.assertTrue(so_line5.project_id.allow_timesheets, "Create project should allow timesheets")
self.assertEqual(so_line4.task_id.project_id, so_line2.project_id, "Task created with line 4 should have the project based on template A of the SO.")
self.assertEqual(so_line1.project_id.sale_line_id, so_line1, "SO line of project with template A should be the one that create it.")
self.assertEqual(so_line2.project_id.sale_line_id, so_line2, "SO line of project should be the one that create it.")
self.assertEqual(so_line5.project_id.sale_line_id, so_line5, "SO line of project with template B should be the one that create it.")
def test_sale_task_in_project_with_project(self):
""" This will test the new 'task_in_project' service tracking correctly creates tasks and projects
when a project_id is configured on the parent sale_order (ref task #1915660).
Setup:
- Configure a project_id on the SO
- SO line 1: a product with its delivery tracking set to 'task_in_project'
- SO line 2: the same product as SO line 1
- SO line 3: a product with its delivery tracking set to 'project_only'
- Confirm sale_order
Expected result:
- 2 tasks created on the project_id configured on the SO
- 1 project created with the correct template for the 'project_only' product
"""
self.sale_order.write({'project_id': self.project_global.id})
self.sale_order._onchange_project_id()
self.assertEqual(self.sale_order.analytic_account_id, self.analytic_account_sale, "Changing the project on the SO should set the analytic account accordingly.")
so_line1 = self.env['sale.order.line'].create({
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 11,
'order_id': self.sale_order.id,
})
so_line2 = self.env['sale.order.line'].create({
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
so_line3 = self.env['sale.order.line'].create({
'product_id': self.product_order_timesheet4.id,
'product_uom_qty': 5,
'order_id': self.sale_order.id,
})
# temporary project_template_id for our checks
self.product_order_timesheet4.write({
'project_template_id': self.project_template.id
})
self.sale_order.action_confirm()
# remove it after the confirm because other tests don't like it
self.product_order_timesheet4.write({
'project_template_id': False
})
self.assertTrue(so_line1.task_id, "so_line1 should create a task as its product's service_tracking is set as 'task_in_project'")
self.assertEqual(so_line1.task_id.project_id, self.project_global, "The project on so_line1's task should be project_global as configured on its parent sale_order")
self.assertTrue(so_line2.task_id, "so_line2 should create a task as its product's service_tracking is set as 'task_in_project'")
self.assertEqual(so_line2.task_id.project_id, self.project_global, "The project on so_line2's task should be project_global as configured on its parent sale_order")
self.assertFalse(so_line3.task_id.name, "so_line3 should not create a task as its product's service_tracking is set as 'project_only'")
self.assertNotEqual(so_line3.project_id, self.project_template, "so_line3 should create a new project and not directly use the configured template")
self.assertIn(self.project_template.name, so_line3.project_id.name, "The created project for so_line3 should use the configured template")
def test_sale_task_in_project_without_project(self):
""" This will test the new 'task_in_project' service tracking correctly creates tasks and projects
when the parent sale_order does NOT have a configured project_id (ref task #1915660).
Setup:
- SO line 1: a product with its delivery tracking set to 'task_in_project'
- Confirm sale_order
Expected result:
- 1 project created with the correct template for the 'task_in_project' because the SO
does not have a configured project_id
- 1 task created from this new project
"""
so_line1 = self.env['sale.order.line'].create({
'product_id': self.product_order_timesheet3.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
# temporary project_template_id for our checks
self.product_order_timesheet3.write({
'project_template_id': self.project_template.id
})
self.sale_order.action_confirm()
# remove it after the confirm because other tests don't like it
self.product_order_timesheet3.write({
'project_template_id': False
})
self.assertTrue(so_line1.task_id, "so_line1 should create a task as its product's service_tracking is set as 'task_in_project'")
self.assertNotEqual(so_line1.project_id, self.project_template, "so_line1 should create a new project and not directly use the configured template")
self.assertIn(self.project_template.name, so_line1.project_id.name, "The created project for so_line1 should use the configured template")
def test_billable_task_and_subtask(self):
""" Test if subtasks and tasks are billed on the correct SO line """
# create SO line and confirm it
so_line_deliver_new_task_project = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
so_line_deliver_new_task_project_2 = self.env['sale.order.line'].create({
'name': self.product_delivery_timesheet3.name + "(2)",
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
project = so_line_deliver_new_task_project.project_id
task = so_line_deliver_new_task_project.task_id
self.assertEqual(project.sale_line_id, so_line_deliver_new_task_project, "The created project should be linked to the so line")
self.assertEqual(task.sale_line_id, so_line_deliver_new_task_project, "The created task should be linked to the so line")
# create a new task and subtask
subtask = self.env['project.task'].create({
'parent_id': task.id,
'project_id': project.id,
'name': '%s: substask1' % (task.name,),
})
task2 = self.env['project.task'].create({
'project_id': project.id,
'name': '%s: substask1' % (task.name,)
})
self.assertEqual(subtask.sale_line_id, task.sale_line_id, "By, default, a child task should have the same SO line as its mother")
self.assertEqual(task2.sale_line_id, project.sale_line_id, "A new task in a billable project should have the same SO line as its project")
self.assertEqual(task2.partner_id, so_line_deliver_new_task_project.order_partner_id, "A new task in a billable project should have the same SO line as its project")
# moving subtask in another project
subtask.write({'project_id': self.project_global.id})
self.assertEqual(subtask.sale_line_id, task.sale_line_id, "A child task should always have the same SO line as its mother, even when changing project")
self.assertEqual(subtask.sale_line_id, so_line_deliver_new_task_project)
# changing the SO line of the mother task
task.write({'sale_line_id': so_line_deliver_new_task_project_2.id})
self.assertEqual(subtask.sale_line_id, so_line_deliver_new_task_project, "A child task is not impacted by the change of SO line of its mother")
self.assertEqual(task.sale_line_id, so_line_deliver_new_task_project_2, "A mother task can have its SO line set manually")
# changing the SO line of a subtask
subtask.write({'sale_line_id': so_line_deliver_new_task_project_2.id})
self.assertEqual(subtask.sale_line_id, so_line_deliver_new_task_project_2, "A child can have its SO line set manually")
def test_change_ordered_qty(self):
""" Changing the ordered quantity of a SO line that have created a task should update the planned hours of this task """
sale_order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 50,
})
self.sale_order.action_confirm()
self.assertEqual(sale_order_line.product_uom_qty, sale_order_line.task_id.allocated_hours, "The planned hours should be the same as the ordered quantity of the native SO line")
sale_order_line.write({'product_uom_qty': 20})
self.assertEqual(sale_order_line.product_uom_qty, sale_order_line.task_id.allocated_hours, "The planned hours should have changed when updating the ordered quantity of the native SO line")
self.sale_order._action_cancel()
sale_order_line.write({'product_uom_qty': 30})
self.assertEqual(sale_order_line.product_uom_qty, sale_order_line.task_id.allocated_hours, "The planned hours should have changed when updating the ordered quantity, even after SO cancellation")
self.sale_order.action_lock()
with self.assertRaises(UserError):
sale_order_line.write({'product_uom_qty': 20})
def test_copy_billable_project_and_task(self):
sale_order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 5,
})
self.sale_order.action_confirm()
task = self.env['project.task'].search([('sale_line_id', '=', sale_order_line.id)])
project = sale_order_line.project_id
# copy the project
project_copy = project.copy()
self.assertFalse(project_copy.sale_line_id, "Duplicating project should erase its Sale line")
self.assertFalse(project_copy.sale_order_id, "Duplicating project should erase its Sale order")
self.assertEqual(len(project.tasks), len(project_copy.tasks), "Copied project must have the same number of tasks")
self.assertFalse(project_copy.tasks.mapped('sale_line_id'), "The tasks of the duplicated project should not have a Sale Line set.")
# copy the task
task_copy = task.copy()
self.assertEqual(task_copy.sale_line_id, task.sale_line_id, "Duplicating task should keep its Sale line")
def test_remaining_hours_prepaid_services(self):
""" Test if the remaining hours is correctly computed
Test Case:
=========
1) Check the remaining hours in the SOL containing a prepaid service product,
2) Create task in project with pricing type is equal to "task rate" and has the customer in the SO
and check if the remaining hours is equal to the remaining hours in the SOL,
3) Create timesheet in the task for this SOL and check if the remaining hours correctly decrease,
4) Change the SOL in the task and see if the remaining hours is correctly recomputed.
5) Create without storing the timesheet to check if remaining hours in SOL does not change.
"""
# 1) Check the remaining hours in the SOL containing a prepaid service product
prepaid_service_sol = self.so.order_line.filtered(lambda sol: sol.product_id.service_policy == 'ordered_prepaid')
self.assertEqual(len(prepaid_service_sol), 1, "It should only have one SOL with prepaid service product in this SO.")
self.assertEqual(prepaid_service_sol.remaining_hours, prepaid_service_sol.product_uom_qty - prepaid_service_sol.qty_delivered, "The remaining hours of this SOL should be equal to the ordered quantity minus the delivered quantity.")
# 2) Create task in project with pricing type is equal to "task rate" and has the customer in the SO
# and check if the remaining hours is equal to the remaining hours in the SOL,
task = self.env['project.task'].create({
'name': 'Test task',
'project_id': self.project_task_rate.id,
})
self.assertEqual(task.partner_id, self.project_task_rate.partner_id)
self.assertEqual(task.partner_id, self.so.partner_id)
self.assertEqual(task.remaining_hours_so, prepaid_service_sol.remaining_hours)
# 3) Create timesheet in the task for this SOL and check if the remaining hours correctly decrease
self.env['account.analytic.line'].create({
'name': 'Test Timesheet',
'project_id': self.project_task_rate.id,
'task_id': task.id,
'unit_amount': 1,
'employee_id': self.employee_user.id,
})
self.assertEqual(task.remaining_hours_so, 1, "Before the creation of a timesheet, the remaining hours was 2 hours, when we timesheet 1 hour, the remaining hours should be equal to 1 hour.")
self.assertEqual(prepaid_service_sol.remaining_hours, task.remaining_hours_so, "The remaining hours on the SOL should also be equal to 1 hour.")
# 4) Change the SOL in the task and see if the remaining hours is correctly recomputed.
task.update({
'sale_line_id': self.so.order_line[0].id,
})
self.assertEqual(task.remaining_hours_so, False, "Since the SOL doesn't contain a prepaid service product, the remaining_hours_so should be equal to False.")
self.assertEqual(prepaid_service_sol.remaining_hours, 2, "Since the timesheet on task has the same SOL than the one in the task, the remaining_hours should increase of 1 hour to be equal to 2 hours.")
# 5) Create without storing the timesheet to check if remaining hours in SOL does not change
timesheet = self.env['account.analytic.line'].new({
'name': 'Test Timesheet',
'project_id': self.project_task_rate.id,
'task_id': task.id,
'unit_amount': 1,
'so_line': prepaid_service_sol.id,
'is_so_line_edited': True,
'employee_id': self.employee_user.id,
})
self.assertEqual(timesheet.so_line, prepaid_service_sol, "The SOL should be the same than one containing the prepaid service product.")
self.assertEqual(prepaid_service_sol.remaining_hours, 2, "The remaining hours should not change.")
def test_several_uom_sol_to_planned_hours(self):
allocated_hours_for_uom = {
'day': 8.0,
'hour': 1.0,
'unit': 1.0,
'gram': 0.0,
}
project = self.project_global.copy({'tasks': False})
Product = self.env['product.product']
product_vals = {
'type': 'service',
'service_type': 'timesheet',
'project_id': project.id,
'service_tracking': 'task_global_project',
}
SaleOrderLine = self.env['sale.order.line']
sol_vals = {
'product_uom_qty': 1,
'price_unit': 100,
'order_id': self.sale_order.id,
}
for uom_name in allocated_hours_for_uom:
uom_id = self.env.ref('uom.product_uom_%s' % uom_name)
product_vals.update({
'name': uom_name,
'uom_id': uom_id.id,
'uom_po_id': uom_id.id,
})
product = Product.create(product_vals)
sol_vals.update({
'name': uom_name,
'product_id': product.id,
'product_uom': uom_id.id,
})
SaleOrderLine.create(sol_vals)
self.sale_order.action_confirm()
tasks = project.task_ids
for task in tasks:
self.assertEqual(task.allocated_hours, allocated_hours_for_uom[task.sale_line_id.name])
def test_add_product_analytic_account(self):
""" When we have a project with an analytic account and we add a product to the task,
the consequent invoice line should have the same analytic account as the project.
"""
# Ensure the SO has no analytic account to give to its SOLs
self.assertFalse(self.sale_order.analytic_account_id)
Product = self.env['product.product']
SaleOrderLine = self.env['sale.order.line']
# Create a SO with a service that creates a task
product_create = Product.create({
'name': 'Product that creates the task',
'type': 'service',
'service_type': 'timesheet',
'project_id': self.project_global.id,
'service_tracking': 'task_global_project',
})
sale_order_line_create = SaleOrderLine.create({
'order_id': self.sale_order.id,
'name': product_create.name,
'product_id': product_create.id,
'product_uom_qty': 5,
'product_uom': product_create.uom_id.id,
'price_unit': product_create.list_price,
})
self.sale_order.action_confirm()
# Add a SOL with a task_id to mimmic the "Add a product" flow on the task
product_add = Product.create({'name': 'Product added on task'})
SaleOrderLine.create({
'order_id': self.sale_order.id,
'name': product_add.name,
'product_id': product_add.id,
'product_uom_qty': 5,
'product_uom': product_add.uom_id.id,
'price_unit': product_add.list_price,
'task_id': sale_order_line_create.task_id.id,
})
self.sale_order._create_invoices()
# Check that the resulting invoice line and the project have the same analytic account
invoice_line = self.sale_order.invoice_ids.line_ids.filtered(lambda line: line.product_id == product_add)
self.assertEqual(invoice_line.analytic_distribution, {str(self.project_global.analytic_account_id.id): 100},
"SOL's analytic distribution should contain the project analytic account")
def test_sale_timesheet_invoice(self):
""" Test timesheet is correctly linked to an invoice when its SOL is invoiced
Test Cases:
==========
1) Create a SOL on a SO
2) Confirm the SO
3) Set the SOL on a new timesheet
4) Create an invoice for this SO
5) Check the timesheet is linked to the invoice
"""
so_line = self.env['sale.order.line'].create({
'product_id': self.product_delivery_timesheet2.id,
'product_uom_qty': 10,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
timesheet = self.env['account.analytic.line'].create({
'name': 'Test Line',
'project_id': so_line.task_id.project_id.id,
'task_id': so_line.task_id.id,
'unit_amount': 5,
'employee_id': self.employee_manager.id,
})
self.assertFalse(timesheet.timesheet_invoice_id)
invoice = self.sale_order._create_invoices()
invoice.action_post()
self.assertEqual(invoice, timesheet.timesheet_invoice_id)
def test_prevent_update_project_allocated_hours_after_confirming_quotation(self):
""" Test allocated hours in the project linked to a SO is not automatically updated
When the project is linked to a SO (confirmed quotation) the allocated
hours should not be recomputed when the quantity ordered of a product
is changed in the SO.
Test Cases:
==========
1) Create a SOL on a SO
2) Confirm the SO
3) Store the project allocated hour
4) Modify the SOL product qty
5) Check the project allocated hour is modify
"""
order_line = self.env['sale.order.line'].create({
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 8,
})
self.sale_order.action_confirm()
allocated_hours = order_line.project_id.allocated_hours
order_line.product_uom_qty = 10
self.assertEqual(allocated_hours, order_line.project_id.allocated_hours, 'Project allocated hours should not be changed.')
def test_different_uom_to_hours_on_sale_order_confirmation(self):
""" Verify correctness of a project's allocted hours for multiple UOMs.
The conversion to time should be processed as follows :
H : qty = uom_qty [Hours]
D : qty = uom_qty * 8 [Hours]
U : qty = uom_qty [Hours]
Other : qty = 0
Test Cases:
==========
1) Create a 4 SOL on a SO With different UOM
2) Confirm the SO
3) Check the project allocated hour is correctly set
4) Repeat with different timesheet encoding UOM
"""
self.env['sale.order.line'].create([{
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 2,
'product_uom': self.env.ref('uom.product_uom_day').id, # 16 hours
}, {
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 8,
'product_uom': self.env.ref('uom.product_uom_hour').id, # 8 hours
}, {
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 1,
'product_uom': self.env.ref('uom.product_uom_dozen').id, # 0 hours
}, {
'order_id': self.sale_order.id,
'product_id': self.product_delivery_timesheet3.id,
'product_uom_qty': 6,
'product_uom': self.env.ref('uom.product_uom_unit').id, # 6 hours
}])
self.sale_order.action_confirm()
allocated_hours = self.sale_order.project_ids.allocated_hours
self.assertEqual(16 + 8 + 6, allocated_hours,
"Project's allocated hours should add up correctly.")
self.env.company.timesheet_encode_uom_id = self.env.ref('uom.product_uom_day')
so_copy = self.sale_order.copy()
so_copy.action_confirm()
self.assertEqual(allocated_hours, so_copy.project_ids.allocated_hours,
"Timesheet encoding shouldn't affect hours allocated.")
def test_compute_project_and_task_button_with_ts(self):
""" This test ensures that the button are correctly computed when there is a timesheet service product on a SO. The behavior was not modified in sale_timesheet, but since
the timesheet product case can not be tested in sale_project, we have to add the test here."""
sale_order_1 = self.env['sale.order'].with_context(tracking_disable=True).create([{
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
}])
# delivered timesheet
line_1 = self.env['sale.order.line'].create({
'product_id': self.product_service_delivered_timesheet.id,
'order_id': sale_order_1.id,
})
sale_order_1.action_confirm()
self.assertTrue(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' on the sale order, the button should be displayed")
self.assertFalse(sale_order_1.show_project_button, "There is no project on the sale order, the button should be hidden")
self.assertFalse(sale_order_1.show_task_button, "There is no project on the sale order, the button should be hidden")
line_1.project_id = self.project_global.id
sale_order_1._compute_show_project_and_task_button()
self.assertFalse(sale_order_1.show_create_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be hidden")
self.assertTrue(sale_order_1.show_project_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
self.assertTrue(sale_order_1.show_task_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
def test_compute_show_timesheet_button(self):
""" This test ensures that the hours recorded button is correctly computed. If there is a service product with an invoice policy of prepaid or timesheet, and there is
at least on project linked to the SO, then the button should be displayed """
sale_order_1, sale_order_2 = self.env['sale.order'].with_context(tracking_disable=True).create([{
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
}, {
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
}])
# consumable product, delivered milestone, and delivered manual
self.env['sale.order.line'].create([{
'product_id': self.product_consumable.id,
'order_id': sale_order_1.id,
}, {
'product_id': self.product_service_delivered_milestone.id,
'order_id': sale_order_1.id,
}, {
'product_id': self.product_service_delivered_manual.id,
'order_id': sale_order_1.id,
}])
sale_order_1.action_confirm()
self.assertFalse(sale_order_1.show_hours_recorded_button, "There is no service product service with the correct service_policy set on 'delivered on timesheet' on the sale order, the button should be hidden")
# adds a delivered timesheet product to the SO
line_4 = self.env['sale.order.line'].create({
'product_id': self.product_service_delivered_timesheet.id,
'order_id': sale_order_1.id,
})
self.assertFalse(sale_order_1.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden")
line_4.project_id = self.project_global
sale_order_1._compute_show_hours_recorded_button()
self.assertTrue(sale_order_1.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
line_1 = self.env['sale.order.line'].create({
'product_id': self.product_service_ordered_prepaid.id,
'order_id': sale_order_2.id,
})
sale_order_2.action_confirm()
self.assertFalse(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden")
line_1.project_id = self.project_global
sale_order_2._compute_show_hours_recorded_button()
self.assertTrue(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")
# remove the project from the so and ensure the SO is back to its previous state
line_1.project_id = False
sale_order_2._compute_show_hours_recorded_button()
self.assertFalse(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' but no project on the sale order, the button should be hidden")
# adds a task whose sale item is a sale order line from the SO, and adds a timesheet in that task. This should enable the display of the button
task = self.env['project.task'].create({
'name': 'Test Task',
'project_id': self.project_global.id,
'sale_line_id': line_1.id,
})
self.env['account.analytic.line'].create({
'name': "timesheet",
'unit_amount': 5,
'project_id': task.project_id.id,
'task_id': task.id,
'employee_id': self.employee_user.id,
})
sale_order_2._compute_timesheet_count()
sale_order_2._compute_show_hours_recorded_button()
self.assertTrue(sale_order_2.show_hours_recorded_button, "There is a product service with the service_policy set on 'delivered on timesheet' and a project on the sale order, the button should be displayed")