[MERGE] sale_timesheet: add some unit tests + clean code
Purpose
======
The goal of this PR is to write some unit tests about the changes made in Task-2388500, Task-2409761 and Task-2424382 to keep the functional features.
## Determined Sales Order Items in task and timesheets
These tests check if the Sales Order Items (SOL) computed in task and timesheets are the ones expected based on the pricing type defined in the project.
- Check if the SOL in task is the same than the one in project when we create a task in project which has a pricing type to project rate or employee rate.
- When the pricing type is employee rate and we timesheet, check if the SOL on these timesheets are the one expected, that is:
- If the pricing type is employee rate and there are some mapping for employees, normally when we timesheet for an employee in the mapping, we should have the SOL defined in the mapping for this employee in the timesheet.
- If we timesheet for an employee who is not in the mapping then we should have the SOL of the current task.
- When the pricing type is task rate and we defined a customer who has some SOs with services products in project, if we create a task in this project then we should have the customer defined in the project and the one of latest SOLs if it is prepaid service and remaining_hours > 0. Otherwise, the SOL is False.
- When the pricing type is employee rate or project rate and the customer, SO, SOL is defined in the project, if we change the customer then the SO and SOL should be False if the customer in the project is different that the one in SO selected.
- If the pricing type is employee rate then when the customer changed in the project, the SOL in each mapping should also be removed.
## Remaining hours for prepaid service products
This test checks if the remaining hours is correctly computed in project.task when the Sales Order Item in this task contains a prepaid service product.
## Tour JS to check features in the UI
A tour js has been created to check the process with the UI. The steps are the following:
- Go to the Sales App,
- Create a sales order for Brandon Freeman for instance,
- In this Sales Order, we take a prepaid service product (we created this one in a unit test in python as we must not use the demo data to realize the JS tour) and the quantity ordered is 10 hours,
- Check the product_uom field is set on 'Hours',
- Confirm the Quotation to create the Sales Order,
- Go to the Project App,
- Create billable project to timesheet for Brandon Freeman,
- Create task in this project and add a timesheet into it,
- Show in this task that we can see the Sales Order Item linked to the createed timesheet, because this field is hidden by default.
- Go to the Settings menu and then Projects submenu.
- Create a project, and show the three different configurations to invoice services in a project. That is:
- Task rate
- Project rate
- Employee rate
## Miscellaneous
The `project.task.create.sale.order` model has been removed because the wizard is unused since these 3 tasks have been merged.
task-2439304
--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
closes odoo/odoo#65146
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
@@ -31,7 +31,6 @@ have real delivered quantities in sales orders.
|
||||
'wizard/project_create_sale_order_views.xml',
|
||||
'wizard/project_create_invoice_views.xml',
|
||||
'wizard/sale_make_invoice_advance_views.xml',
|
||||
'wizard/project_task_create_sale_order_views.xml'
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/sale_project_templates.xml',
|
||||
|
||||
@@ -5,4 +5,3 @@ access_project_sale_line_employee_map_manager,access_project_sale_line_employee_
|
||||
access_project_create_sale_order,access.project.create.sale.order,model_project_create_sale_order,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_project_create_sale_order_line,access.project.create.sale.order.line,model_project_create_sale_order_line,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_project_create_invoice,access.project.create.invoice,model_project_create_invoice,sales_team.group_sale_salesman_all_leads,1,1,1,0
|
||||
access_project_task_create_sale_order,access.project.task.create.sale.order,model_project_task_create_sale_order,sales_team.group_sale_salesman,1,1,1,0
|
||||
|
||||
|
@@ -0,0 +1,262 @@
|
||||
odoo.define('sale_timesheet.tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
const tour = require('web_tour.tour');
|
||||
|
||||
tour.register('sale_timesheet_tour', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
}, [...tour.stepUtils.goToAppSteps("sale.sale_menu_root", 'Go to the Sales App'),
|
||||
{
|
||||
trigger: 'button.o_list_button_add',
|
||||
content: 'Click on CREATE button to create a quotation with service products.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="partner_id"]',
|
||||
content: 'Add the customer for this quotation (e.g. Brandon Freeman)',
|
||||
run: function (actions) {
|
||||
actions.text('Brandon Freeman', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.o_partner_autocomplete_dropdown > li:first-child > a',
|
||||
content: 'Select the first item on the autocomplete dropdown',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
trigger: 'td.o_field_x2many_list_row_add > a:first-child',
|
||||
content: 'Click on "Add a product" to add a new product. We will add a service product.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: '.o_field_widget[name="product_id"], .o_field_widget[name="product_template_id"]',
|
||||
content: 'Select a prepaid service product <i>(e.g. Service Product (Prepaid Hours))</i>',
|
||||
run: function (actions) {
|
||||
actions.text('Service Product (Prepaid Hours)', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.ui-menu.ui-widget.ui-autocomplete > li:first-child > a',
|
||||
content: 'Select the prepaid service product in the autocomplete dropdown',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input[name="product_uom_qty"]',
|
||||
content: "Add 10 hours as ordered quantity for this product.",
|
||||
run: 'text 10',
|
||||
}, {
|
||||
trigger: 'button[name="action_confirm"]',
|
||||
content: 'Click on Confirm button to create a sale order with this quotation.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'button.o_form_button_save',
|
||||
content: 'Click on Save button to save the Sales Order.',
|
||||
run: 'click',
|
||||
}, tour.stepUtils.toggleHomeMenu(),
|
||||
...tour.stepUtils.goToAppSteps("project.menu_main_pm", 'Go to the Project app.'),
|
||||
{
|
||||
trigger: 'button.o-kanban-button-new',
|
||||
content: 'Add a new project.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input.o_field_widget.o_project_name',
|
||||
content: 'Select your project name (e.g. Project for Freeman)',
|
||||
run: function (actions) {
|
||||
actions.text('Project for Freeman', this.$anchor);
|
||||
},
|
||||
}, {
|
||||
trigger: 'div[name="allow_billable"] input',
|
||||
content: 'Click on the checkbox to have a billable project.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'button[name="action_view_tasks"]',
|
||||
content: 'Click on Create button to create and enter to this newest project.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div.o_kanban_header > div:first-child',
|
||||
content: 'Select a name of your kanban column (e.g. To Do)',
|
||||
run: function (actions) {
|
||||
actions.text('To Do', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'button.o_kanban_add',
|
||||
content: 'Click on Add button to create the column.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'button.o-kanban-button-new',
|
||||
content: 'Click on Create button to create a task into your project.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input[name="name"]',
|
||||
content: 'Select the name of the task (e.g. Onboarding)',
|
||||
run: function (actions) {
|
||||
actions.text('Onboarding', this.$anchor);
|
||||
}
|
||||
}, {
|
||||
trigger: 'button.o_kanban_edit',
|
||||
content: 'Click on Edit button to enter to the form view of the task.',
|
||||
position: 'bottom',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="partner_id"]',
|
||||
content: 'Select the customer of your Sales Order <i>(e.g. Brandon Freeman)</i>. Since we have a Sales Order for this customer with a prepaid service product which the remaining hours to deliver is greater than 0, the Sales Order Item in the task should be contain the Sales Order Item containing this prepaid service product.',
|
||||
run: function (actions) {
|
||||
actions.text('Brandon Freeman', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.o_partner_autocomplete_dropdown > li:first-child > a',
|
||||
content: 'Select the customer in the autocomplete dropdown.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div.o_notebook_headers',
|
||||
content: 'Click on Timesheets page to log a timesheet',
|
||||
run: function (actions) {
|
||||
const notebookId = $('div[name="timesheet_ids"]').closest("div.tab-pane").attr('id');
|
||||
actions.click(this.$anchor.find(`a[data-toggle="tab"][href="#${notebookId}"]`));
|
||||
},
|
||||
}, {
|
||||
trigger: 'div[name="timesheet_ids"] td.o_field_x2many_list_row_add a[role="button"]',
|
||||
content: 'Click on Add a line to create a new timesheet into the task.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input[name="unit_amount"]',
|
||||
content: 'Enter one hour for this timesheet',
|
||||
run: function (actions) {
|
||||
actions.text('1', this.$anchor);
|
||||
},
|
||||
}, {
|
||||
trigger: 'i.o_optional_columns_dropdown_toggle',
|
||||
content: 'The so_line field should be hidden by default. We check if it is the case by adding this field in the timesheet list view',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input[name="so_line"]',
|
||||
content: 'Check the so_line field to display the column on the list view.',
|
||||
run: function (actions) {
|
||||
if (!this.$anchor.prop('checked')) {
|
||||
actions.click(this.$anchor);
|
||||
}
|
||||
},
|
||||
}, {
|
||||
trigger: 'button[name="action_view_so"]',
|
||||
content: 'Click on this stat button to see the SO linked to the SOL of the task.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="order_line"]',
|
||||
content: 'Check if the quantity delivered is equal to 1 hour.',
|
||||
run: function () {
|
||||
const $header = this.$anchor.find('thead > tr');
|
||||
if (!$header || $header.length === 0)
|
||||
console.error('No Sales Order Item is found in the Sales Order.');
|
||||
const tr = $header[0];
|
||||
let index = -1;
|
||||
for (let i = 0; i < tr.children.length; i++) {
|
||||
const th = tr.children.item(i);
|
||||
if (th.dataset && th.dataset.name === 'qty_delivered')
|
||||
index = i;
|
||||
}
|
||||
const qtyDelivered = this.$anchor.find(`tbody > tr:first-child > td.o_data_cell:eq(${index})`).text();
|
||||
if (qtyDelivered !== "1.00")
|
||||
console.error('The quantity delivered on this Sales Order Item should be equal to 1.00 hour. qtyDelivered = ' + qtyDelivered);
|
||||
},
|
||||
}, {
|
||||
trigger: 'a[data-menu-xmlid="project.menu_project_config"]',
|
||||
content: 'Click on the Configuration menu.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'a[role="menuitem"][data-menu-xmlid="project.menu_projects_config"]',
|
||||
content: 'Select Configuration > Projects.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'button.o_list_button_add',
|
||||
content: 'Click on Create button to create a new project and see the different configuration available for the project.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div.o_notebook_headers',
|
||||
content: 'Click on Settings page to check the allow_billable checkbox',
|
||||
run: function (actions) {
|
||||
const notebookId = $('div[name="allow_billable"]').closest("div.tab-pane").attr('id');
|
||||
actions.click(this.$anchor.find(`a[data-toggle="tab"][href="#${notebookId}"]`));
|
||||
},
|
||||
}, {
|
||||
trigger: 'div[name="allow_billable"] > input',
|
||||
content: 'Check the allow_billable',
|
||||
run: function (actions) {
|
||||
if (!this.$anchor.prop('checked')) {
|
||||
actions.click(this.$anchor);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
trigger: 'div.o_notebook_headers',
|
||||
content: 'Click on Invoicing tab to configure the invoicing of this project.',
|
||||
run: function (actions) {
|
||||
const notebookId = $('div[name="pricing_type"]').closest("div.tab-pane").attr('id');
|
||||
actions.click(this.$anchor.find(`a[data-toggle="tab"][href="#${notebookId}"]`));
|
||||
},
|
||||
}, {
|
||||
trigger: 'div[name="pricing_type"]',
|
||||
content: 'Check if the pricing type is equal to Task Rate',
|
||||
run: function (actions) {
|
||||
const pricingType = this.$anchor.find('input[data-value="task_rate"]').attr('checked');
|
||||
if (!pricingType)
|
||||
console.error('The default pricing type of the project should be "Task Rate".');
|
||||
}
|
||||
}, {
|
||||
trigger: 'input[data-value="fixed_rate"]',
|
||||
content: 'Change the pricing type to "Project Rate"',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="partner_id"]',
|
||||
content: 'Add the customer for this project to select an SO and SOL for this customer <i>(e.g. Brandon Freeman)</i>.',
|
||||
run: function (actions) {
|
||||
actions.text('Azure Interior, Brandon Freeman', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.o_partner_autocomplete_dropdown > li:first-child > a',
|
||||
content: 'Select the customer in the autocomplete dropdown',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="sale_order_id"]',
|
||||
content: 'Select the first Sales Order for this customer to select a SOL.',
|
||||
run: function (actions) {
|
||||
actions.text('S', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.ui-autocomplete > li:first-child > a',
|
||||
content: 'Select the Sales Order in autocomplete dropdown.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="sale_line_id"]',
|
||||
content: 'Select a Sales Order Item as Default Sales Order Item for each task in this project.',
|
||||
run: function (actions) {
|
||||
actions.text('S', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.ui-autocomplete > li:first-child > a',
|
||||
content: 'Select the Sales Order Item in the autocomplete dropdown.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'input[data-value="employee_rate"]',
|
||||
content: 'Select "Employee rate" as pricing type.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="sale_line_employee_ids"] td.o_field_x2many_list_row_add > a[role="button"]',
|
||||
content: 'Click on Add a line on the mapping list view.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="sale_line_employee_ids"] div[name="employee_id"] input',
|
||||
content: 'Select an employee to link a Sales Order Item on his timesheets into this project.',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'ul.ui-autocomplete > li:first-child > a',
|
||||
content: 'Select the first employee in the autocomplete dropdown',
|
||||
run: 'click',
|
||||
}, {
|
||||
trigger: 'div[name="sale_line_employee_ids"] div[name="sale_line_id"]',
|
||||
content: 'Select the Sales Order Item to link to the timesheets of this employee.',
|
||||
position: 'bottom',
|
||||
run: function (actions) {
|
||||
actions.text('S', this.$anchor.find('input'));
|
||||
},
|
||||
}, {
|
||||
trigger: 'ul.ui-autocomplete > li:first-child > a',
|
||||
content: 'Select the first Sales Order Item in the autocomplete dropdown.',
|
||||
run: 'click',
|
||||
}]);
|
||||
|
||||
});
|
||||
@@ -10,3 +10,6 @@ from . import test_reporting
|
||||
from . import test_project_overview
|
||||
from . import test_project_billing_multicompany
|
||||
from . import test_upsell_warning
|
||||
from . import test_edit_so_line_timesheet
|
||||
from . import test_so_line_determined_in_timesheet
|
||||
from . import test_sale_timesheet_ui
|
||||
|
||||
@@ -67,13 +67,14 @@ class TestCommonSaleTimesheet(TestSaleCommon):
|
||||
})
|
||||
|
||||
# Create projects
|
||||
cls.project_global = cls.env['project.project'].create({
|
||||
Project = cls.env['project.project'].with_context(tracking_disable=True)
|
||||
cls.project_global = Project.create({
|
||||
'name': 'Project for selling timesheets',
|
||||
'allow_timesheets': True,
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
'allow_billable': True,
|
||||
})
|
||||
cls.project_template = cls.env['project.project'].create({
|
||||
cls.project_template = Project.create({
|
||||
'name': 'Project TEMPLATE for services',
|
||||
'allow_timesheets': True,
|
||||
})
|
||||
@@ -82,6 +83,36 @@ class TestCommonSaleTimesheet(TestSaleCommon):
|
||||
'sequence': 1,
|
||||
'project_ids': [(4, cls.project_template.id)]
|
||||
})
|
||||
# Projects: at least one per billable type
|
||||
cls.project_task_rate = Project.create({
|
||||
'name': 'Project with pricing_type="task_rate"',
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'partner_id': cls.partner_b.id,
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
})
|
||||
cls.project_project_rate = cls.project_task_rate.copy({
|
||||
'name': 'Project with pricing_type="project_rate"',
|
||||
'pricing_type': 'fixed_rate',
|
||||
})
|
||||
cls.project_employee_rate = cls.project_task_rate.copy({
|
||||
'name': 'Project with pricing_type="employee_rate"',
|
||||
'pricing_type': 'employee_rate',
|
||||
})
|
||||
|
||||
cls.project_subtask = Project.create({
|
||||
'name': "Sub Task Project (non billable)",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': False,
|
||||
'partner_id': False,
|
||||
})
|
||||
cls.project_non_billable = Project.create({
|
||||
'name': "Non Billable Project",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': False,
|
||||
'partner_id': False,
|
||||
'subtask_project_id': cls.project_subtask.id,
|
||||
})
|
||||
|
||||
# Create service products
|
||||
uom_hour = cls.env.ref('uom.product_uom_hour')
|
||||
@@ -319,3 +350,31 @@ class TestCommonSaleTimesheet(TestSaleCommon):
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.so = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': self.partner_b.id,
|
||||
'partner_invoice_id': self.partner_b.id,
|
||||
'partner_shipping_id': self.partner_b.id,
|
||||
})
|
||||
self.env['sale.order.line'].create([{
|
||||
'order_id': self.so.id,
|
||||
'name': self.product_delivery_timesheet1.name,
|
||||
'product_id': self.product_delivery_timesheet1.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': self.product_delivery_timesheet1.list_price
|
||||
}, {
|
||||
'order_id': self.so.id,
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 5,
|
||||
}, {
|
||||
'order_id': self.so.id,
|
||||
'product_id': self.product_delivery_timesheet3.id,
|
||||
'product_uom_qty': 5,
|
||||
}, {
|
||||
'order_id': self.so.id,
|
||||
'product_id': self.product_order_timesheet1.id,
|
||||
'product_uom_qty': 2,
|
||||
}])
|
||||
self.so.action_confirm()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import TestCommonSaleTimesheet
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestEditSoLineTimesheet(TestCommonSaleTimesheet):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.task_rate_task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'sale_line_id': self.so.order_line[0].id,
|
||||
})
|
||||
|
||||
def test_sol_no_change_if_edited(self):
|
||||
""" Check if a sol manually edited, does not change with a change of sol in the task.
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) create some timesheets on this task,
|
||||
2) edit a SOL of a timesheet in this task,
|
||||
3) check if the edited SOL has the one selected and is not the one in the task,
|
||||
4) change the sol on the task,
|
||||
5) check if the timesheet in which the sol has manually edited, does not change but the another ones are the case.
|
||||
"""
|
||||
# 1) create some timesheets on this task
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'task_id': self.task_rate_task.id,
|
||||
'unit_amount': 5,
|
||||
'employee_id': self.employee_manager.id
|
||||
})
|
||||
timesheet._compute_so_line()
|
||||
edited_timesheet = timesheet.copy()
|
||||
self.assertTrue(timesheet.so_line == edited_timesheet.so_line == self.task_rate_task.sale_line_id, "SOL in timesheet should be the same than the one in the task.")
|
||||
self.assertEqual(timesheet.unit_amount + edited_timesheet.unit_amount, self.task_rate_task.sale_line_id.qty_delivered, "The quantity timesheeted should be increased the quantity delivered in the linked SOL.")
|
||||
|
||||
# 2) edit a SOL of a timesheet in this task
|
||||
# Remark, we simulate the action done in the task form view
|
||||
edited_timesheet.write({
|
||||
"is_so_line_edited": True,
|
||||
"so_line": self.so.order_line[1].id,
|
||||
})
|
||||
self.so.order_line._compute_qty_delivered()
|
||||
|
||||
# 3) check if the edited SOL has the one selected and is not the one in the task
|
||||
self.assertNotEqual(edited_timesheet.so_line, self.task_rate_task.sale_line_id, "SOL in timesheet should be different than the one in the task.")
|
||||
self.assertEqual(edited_timesheet.so_line, self.so.order_line[1], "SOL in timesheet is the one selected when we manually edit in the timesheet")
|
||||
self.assertEqual(self.task_rate_task.sale_line_id.qty_delivered, timesheet.unit_amount, "The quantity delivered should be the quantity defined in the first timesheet of the task since the so_line in the second timesheet has manually been changed.")
|
||||
|
||||
# 4) change the sol on the task
|
||||
self.task_rate_task.update({
|
||||
'sale_line_id': self.so.order_line[-1].id,
|
||||
})
|
||||
timesheet._compute_so_line()
|
||||
edited_timesheet._compute_so_line()
|
||||
self.so.order_line._compute_qty_delivered()
|
||||
|
||||
# 5) check if the timesheet in which the sol has manually edited, does not change but the another ones are the case.
|
||||
self.assertEqual(timesheet.so_line, self.task_rate_task.sale_line_id, "SOL in timesheet should be the same than the one in the task.")
|
||||
self.assertNotEqual(edited_timesheet.so_line, self.task_rate_task.sale_line_id, "SOL in timesheet which is manually edited should be different than the one in the task.")
|
||||
self.assertEqual(edited_timesheet.so_line, self.so.order_line[1], "SOL in timesheet should still be the same")
|
||||
@@ -75,29 +75,14 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
})
|
||||
cls.sale_order_2.action_confirm()
|
||||
|
||||
# Projects: at least one per billable type
|
||||
Project = cls.env['project.project'].with_context(tracking_disable=True)
|
||||
cls.project_subtask = Project.create({
|
||||
'name': "Sub Task Project (non billable)",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': False,
|
||||
'partner_id': False,
|
||||
})
|
||||
cls.project_non_billable = Project.create({
|
||||
'name': "Non Billable Project",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': False,
|
||||
'partner_id': False,
|
||||
'subtask_project_id': cls.project_subtask.id,
|
||||
})
|
||||
cls.project_task_rate = cls.env['project.project'].search([('sale_line_id', '=', cls.so2_line_deliver_project_task.id)], limit=1)
|
||||
cls.project_task_rate2 = cls.env['project.project'].search([('sale_line_id', '=', cls.so2_line_deliver_project_template.id)], limit=1)
|
||||
|
||||
cls.project_employee_rate = Project.create({
|
||||
'name': "Project billed at Employee Rate",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'pricing_type': 'employee_rate',
|
||||
cls.project_project_rate.write({
|
||||
'sale_order_id': cls.sale_order_1.id,
|
||||
'sale_line_id': cls.so1_line_order_no_task.id,
|
||||
})
|
||||
cls.project_employee_rate.write({
|
||||
'sale_order_id': cls.sale_order_1.id,
|
||||
'partner_id': cls.sale_order_1.partner_id.id,
|
||||
'subtask_project_id': cls.project_subtask.id,
|
||||
@@ -258,7 +243,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
|
||||
self.assertTrue(task.allow_billable, "Task in project 'employee rate' should be billable")
|
||||
self.assertEqual(task.pricing_type, 'employee_rate', "Task in project 'employee rate' should be billed at employee rate")
|
||||
self.assertFalse(task.sale_line_id, "Task created in a project billed on 'employee rate' should not be linked to a SOL")
|
||||
self.assertEqual(task.sale_line_id, self.project_employee_rate.sale_line_id, "Task created in a project billed on 'employee rate' should be linked to the SOL defined in the project.")
|
||||
self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project")
|
||||
|
||||
# log timesheet on task
|
||||
@@ -303,7 +288,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
task._onchange_project()
|
||||
|
||||
self.assertTrue(task.allow_billable, "Task in project 'task rate' should be billed at task rate")
|
||||
self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task moved in a task rate billable project")
|
||||
self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task, "The SOL in the task should be the one that we selected")
|
||||
self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project")
|
||||
|
||||
# move subtask into task rate project
|
||||
@@ -311,8 +296,8 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
'project_id': self.project_task_rate2.id,
|
||||
})
|
||||
|
||||
self.assertTrue(task.allow_billable, "Subtask should keep the billable type from its parent, even when they are moved into another project")
|
||||
self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Subtask should keep the same sale order line than their mother, even when they are moved into another project")
|
||||
self.assertTrue(subtask.allow_billable, "Subtask should keep the billable type from its parent, even when they are moved into another project")
|
||||
self.assertEqual(subtask.sale_line_id, task.sale_line_id, "Subtask should keep the same sale order line than their mother, even when they are moved into another project")
|
||||
|
||||
# create a second task in employee rate project
|
||||
task2 = Task.with_context(default_project_id=self.project_employee_rate.id).create({
|
||||
@@ -360,8 +345,8 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
})
|
||||
task._onchange_project()
|
||||
|
||||
self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task created in a project billed on 'task rate' should be linked to a SOL of the project")
|
||||
self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'employee rate' should have the same customer as the one from the project")
|
||||
self.assertEqual(task.sale_line_id, self.so2_line_deliver_project_task, "Task created in a project billed on 'task rate' should be linked to a SOL containing a prepaid service product and the remaining hours of this SOL should be greater than 0.")
|
||||
self.assertEqual(task.partner_id, task.project_id.partner_id, "Task created in a project billed on 'task rate' should have the same customer as the one from the project")
|
||||
|
||||
# log timesheet on task
|
||||
timesheet1 = Timesheet.create({
|
||||
@@ -372,7 +357,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
'employee_id': self.employee_manager.id,
|
||||
})
|
||||
|
||||
self.assertEqual(self.project_task_rate.sale_line_id, timesheet1.so_line, "The timesheet should be linked to the SOL associated to the Employee manager in the map")
|
||||
self.assertEqual(task.sale_line_id, timesheet1.so_line, "The timesheet should be linked to the SOL associated to the task since the pricing type of the project is task rate.")
|
||||
|
||||
# create a subtask
|
||||
subtask = Task.with_context(default_project_id=self.project_task_rate.subtask_project_id.id).create({
|
||||
@@ -409,3 +394,30 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
||||
|
||||
self.assertFalse(subtask.sale_line_id, "Subask moved in a employee rate billable project have empty so line")
|
||||
self.assertEqual(subtask.partner_id, task.project_id.partner_id, "Subask created in a project billed on 'employee rate' should have the same customer as the one from the project")
|
||||
|
||||
def test_customer_change_in_project(self):
|
||||
""" Test when the user change the customer in a project
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Take project with pricing_type="fixed_rate", change the existing customer to another and check if the SO and SOL are equal to False.
|
||||
2) Take project with pricing_type="employee_rate", change the existing customer to another and check if the SO and SOL are equal to False.
|
||||
2.1) Check if the SOL in mapping is also equal to False
|
||||
"""
|
||||
# 1) Take project with pricing_type="fixed_rate", change the existing customer to another and check if the SO and SOL are equal to False.
|
||||
self.project_project_rate.write({
|
||||
'partner_id': self.partner_2.id,
|
||||
})
|
||||
self.assertFalse(self.project_project_rate.sale_order_id, "The SO in the project should be False because the previous SO does not for the actual customer of the project.")
|
||||
self.assertFalse(self.project_project_rate.sale_line_id, "The SOL in the project should be False because the SO is removed too.")
|
||||
|
||||
# 2) Take project with pricing_type="employee_rate", change the existing customer to another and check if the SO and SOL are equal to False.
|
||||
self.project_employee_rate.write({
|
||||
'partner_id': self.partner_2.id,
|
||||
})
|
||||
self.assertFalse(self.project_employee_rate.sale_order_id, "The SO in the project should be False because the previous SO does not for the actual customer of the project.")
|
||||
self.assertFalse(self.project_employee_rate.sale_line_id, "The SOL in the project should be False because the SO is removed too.")
|
||||
|
||||
# 2.1) Check if the SOL in mapping is also equal to False
|
||||
self.assertFalse(self.project_employee_rate_manager.sale_line_id, "The SOL in the mapping should be False because the actual customer in the project has not this SOL.")
|
||||
self.assertFalse(self.project_employee_rate_user.sale_line_id, "The SOL in the mapping should be False because the actual customer in the project has not this SOL.")
|
||||
|
||||
@@ -600,3 +600,59 @@ class TestSaleService(TestCommonSaleTimesheet):
|
||||
# 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_timesheet')
|
||||
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,
|
||||
})
|
||||
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,
|
||||
})
|
||||
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.")
|
||||
|
||||
@@ -530,14 +530,15 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
Task = self.env['project.task']
|
||||
today = Date.context_today(self.env.user)
|
||||
|
||||
task = Task.with_context(default_project_id=self.project_global.id).create({
|
||||
task = Task.with_context(default_project_id=self.project_template.id).create({
|
||||
'name': 'first task',
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_id': self.partner_b.id,
|
||||
'planned_hours': 10,
|
||||
'sale_line_id': self.so.order_line[0].id
|
||||
})
|
||||
|
||||
Timesheet.create({
|
||||
'project_id': self.project_global.id,
|
||||
'project_id': self.project_template.id,
|
||||
'task_id': task.id,
|
||||
'name': 'my first timesheet',
|
||||
'unit_amount': 4,
|
||||
@@ -545,51 +546,11 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
|
||||
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
|
||||
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
|
||||
self.assertEqual(timesheet_count1, 1, "One timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 0, "No timesheet in project_template")
|
||||
self.assertEqual(timesheet_count1, 0, "No timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 1, "One timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 1, "The timesheet should be linked to task")
|
||||
|
||||
# change project of task, as the timesheet is not yet invoiced, the timesheet will change his project
|
||||
task.write({
|
||||
'project_id': self.project_template.id
|
||||
})
|
||||
|
||||
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
|
||||
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
|
||||
self.assertEqual(timesheet_count1, 0, "No timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 1, "One timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 1, "The timesheet still should be linked to task")
|
||||
|
||||
wizard = self.env['project.task.create.sale.order'].with_context(active_id=task.id, active_model='project.task').create({
|
||||
'product_id': self.product_delivery_timesheet3.id
|
||||
})
|
||||
|
||||
# We create the SO and the invoice
|
||||
action = wizard.action_create_sale_order()
|
||||
sale_order = self.env['sale.order'].browse(action['res_id'])
|
||||
self.context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': [sale_order.id],
|
||||
'active_id': sale_order.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id
|
||||
}
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(self.context).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
'date_start_invoice_timesheet': today - timedelta(days=4),
|
||||
'date_end_invoice_timesheet': today
|
||||
})
|
||||
wizard.create_invoices()
|
||||
|
||||
Timesheet.create({
|
||||
'project_id': self.project_template.id,
|
||||
'task_id': task.id,
|
||||
'name': 'my second timesheet',
|
||||
'unit_amount': 6,
|
||||
})
|
||||
|
||||
self.assertEqual(Timesheet.search_count([('project_id', '=', self.project_template.id)]), 2, "2 timesheets in project_template")
|
||||
|
||||
# change project of task, the timesheet not yet invoiced will change its project. The timesheet already invoiced will not change his project.
|
||||
task.write({
|
||||
'project_id': self.project_global.id
|
||||
})
|
||||
@@ -597,5 +558,39 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
|
||||
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
|
||||
self.assertEqual(timesheet_count1, 1, "One timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 1, "Still one timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 2, "The 2 timesheet still should be linked to task")
|
||||
self.assertEqual(timesheet_count2, 0, "No timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 1, "The timesheet still should be linked to task")
|
||||
|
||||
# Create an invoice
|
||||
context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': [self.so.id],
|
||||
'active_id': self.so.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id
|
||||
}
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
'date_start_invoice_timesheet': today - timedelta(days=4),
|
||||
'date_end_invoice_timesheet': today,
|
||||
})
|
||||
wizard.create_invoices()
|
||||
|
||||
Timesheet.create({
|
||||
'project_id': self.project_global.id,
|
||||
'task_id': task.id,
|
||||
'name': 'my second timesheet',
|
||||
'unit_amount': 6,
|
||||
})
|
||||
|
||||
self.assertEqual(Timesheet.search_count([('project_id', '=', self.project_global.id)]), 2, "2 timesheets in project_global")
|
||||
|
||||
# change project of task, the timesheet not yet invoiced will change its project. The timesheet already invoiced will not change his project.
|
||||
task.write({
|
||||
'project_id': self.project_template.id
|
||||
})
|
||||
|
||||
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_global.id)])
|
||||
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_template.id)])
|
||||
self.assertEqual(timesheet_count1, 1, "Still one timesheet in project_global")
|
||||
self.assertEqual(timesheet_count2, 1, "One timesheet in project_template")
|
||||
self.assertEqual(len(task.timesheet_ids), 2, "The 2 timesheets still should be linked to task")
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestUi(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
uom_hour_id = cls.env.ref('uom.product_uom_hour').id
|
||||
cls.prepaid_service_product = cls.env['product.product'].create({
|
||||
'name': 'Service Product (Prepaid Hours)',
|
||||
'categ_id': cls.env.ref('product.product_category_3').id,
|
||||
'type': 'service',
|
||||
'list_price': 250.00,
|
||||
'standard_price': 190.00,
|
||||
'uom_id': uom_hour_id,
|
||||
'uom_po_id': uom_hour_id,
|
||||
'service_policy': 'ordered_timesheet',
|
||||
'service_tracking': 'no',
|
||||
})
|
||||
|
||||
def test_ui(self):
|
||||
self.start_tour('/web', 'sale_timesheet_tour', login='admin', timeout=100)
|
||||
@@ -0,0 +1,225 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import TestCommonSaleTimesheet
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSoLineDeterminedInTimesheet(TestCommonSaleTimesheet):
|
||||
|
||||
def test_sol_determined_when_project_is_task_rate(self):
|
||||
""" Test the sol give to the timesheet when the pricing type in the project is task rate
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Create Task in project with pricing_type='task_rate',
|
||||
2) Compute the SOL for the task and check if we have the one containing the prepaid service product,
|
||||
3) Create timesheet and check if the SOL in this timesheet is the one in the task,
|
||||
4) Since the remaining hours of the prepaid service is equals to 0 hour,
|
||||
when we create a new task, the SOL in this one should be equal to False
|
||||
5) Change the SOL in the task and see if the SOL in the timesheet also changes.
|
||||
"""
|
||||
# 1) Create Task in project with pricing_type='task_rate'
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': self.project_task_rate.id,
|
||||
})
|
||||
|
||||
# 2) Compute the SOL for the task and check if we have the one containing the prepaid service product
|
||||
# task._compute_sale_line()
|
||||
self.assertEqual(task.sale_line_id, self.so.order_line[-1], "The SOL in the task should be the one containing the prepaid service product.")
|
||||
self.assertTrue(all(sol.qty_delivered == 0 for sol in self.so.order_line), "The quantity delivered should be equal to 0 because we have no timesheet for each SOL containing in the SO.")
|
||||
|
||||
# 3) Create timesheet and check if the SOL in this timesheet is the one in the task
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 2,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': self.project_task_rate.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
self.assertEqual(timesheet.so_line, task.sale_line_id, "The SOL in the timesheet should be the same than the one in the task.")
|
||||
self.assertEqual(self.so.order_line[-1].qty_delivered, 2, "The quantity delivered should be equal to 2.")
|
||||
self.assertEqual(task.remaining_hours_so, 0, "The remaining hours on the SOL containing the prepaid service product should be equals to 0.")
|
||||
|
||||
# 4) Since the remaining hours of the prepaid service product is equals to 0 hour, when we create a new task, the SOL in this one should be equals to False
|
||||
task2 = self.env['project.task'].create({
|
||||
'name': 'Task 2',
|
||||
'project_id': self.project_task_rate.id,
|
||||
})
|
||||
self.assertFalse(task2.sale_line_id, "The SOL in this task should be equal to False")
|
||||
|
||||
# 5) Change the SOL in the task and see if the SOL in the timesheet also changes.
|
||||
task.update({'sale_line_id': self.so.order_line[0].id})
|
||||
self.assertEqual(timesheet.so_line, task.sale_line_id, "The SOL in the timesheet should also change and be the same than the one in the task.")
|
||||
|
||||
def test_sol_determined_when_project_is_project_rate(self):
|
||||
""" Test the sol give to the timesheet when the pricing type in the project is project rate
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Define a SO and SOL in the project,
|
||||
2) Create task and check if the SOL is the one defined in the project,
|
||||
3) Create timesheet in the task and check if the SOL in the timesheet is the one in the task,
|
||||
4) Change the SOL in the task and check if the SOL in the timesheet has also changed.
|
||||
"""
|
||||
# 1) Define a SO and SOL in the project
|
||||
self.project_project_rate.write({
|
||||
'sale_order_id': self.so.id,
|
||||
'sale_line_id': self.so.order_line[0].id,
|
||||
})
|
||||
|
||||
# 2) Create task and check if the SOL is the one defined in the project
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': self.project_project_rate.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id, self.so.order_line[0], "The SOL in the task should be the one containing the prepaid service product.")
|
||||
self.assertTrue(all(sol.qty_delivered == 0 for sol in self.so.order_line), "The quantity delivered should be equal to 0 because we have no timesheet for each SOL containing in the SO.")
|
||||
|
||||
# 3) Create timesheet in the task and check if the SOL in the timesheet is the one in the task
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 1,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': self.project_project_rate.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
self.assertTrue(timesheet.so_line == task.sale_line_id == self.so.order_line[0], "The SOL in the timesheet should be the same than the one in the task.")
|
||||
self.assertEqual(self.so.order_line[0].qty_delivered, 1, "The quantity delivered should be equal to 1.")
|
||||
|
||||
# 4) Change the SOL in the task and check if the SOL in the timesheet has also changed
|
||||
task.update({'sale_line_id': self.so.order_line[1].id})
|
||||
self.assertTrue(timesheet.so_line == task.sale_line_id == self.so.order_line[1], "The SOL in the timesheet should also change and be the same than the one in the task.")
|
||||
|
||||
def test_sol_determined_when_project_is_employee_rate(self):
|
||||
""" Test the sol give to the timesheet when the pricing type in the project is employee rate
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Define a SO, SOL mapping for an employee in the project,
|
||||
2) Create task and check if the SOL is the one defined in the project,
|
||||
3) Create timesheet in the task and check if the SOL in the timesheet is the one in the task,
|
||||
4) Create timesheet in the task for the employee defined in the mapping and check if the SOL in this timesheet is the one defined in the mapping,
|
||||
5) Change the SOL in the task and check if only the SOL in the timesheet which does not concerne about the mapping changes,
|
||||
6) Change the SOL in the mapping and check if the timesheet conserne by the mapping has its SOL has been changed too.
|
||||
"""
|
||||
# 1) Define a SO, SOL and mapping for an employee in the project,
|
||||
self.project_employee_rate.write({
|
||||
'sale_order_id': self.so.id,
|
||||
'sale_line_id': self.so.order_line[0].id,
|
||||
'sale_line_employee_ids': [(0, 0, {
|
||||
'employee_id': self.employee_user.id,
|
||||
'sale_line_id': self.so.order_line[1].id,
|
||||
})]
|
||||
})
|
||||
mapping = self.project_employee_rate.sale_line_employee_ids
|
||||
|
||||
# 2) Create task and check if the SOL is the one defined in the project,
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'project_id': self.project_employee_rate.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id, self.so.order_line[0], "The SOL in the task should be the one containing the prepaid service product.")
|
||||
self.assertTrue(all(sol.qty_delivered == 0 for sol in self.so.order_line), "The quantity delivered should be equal to 0 because we have no timesheet for each SOL containing in the SO.")
|
||||
|
||||
# 3) Create timesheet in the task and check if the SOL in the timesheet is the one in the task,
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 1,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': self.project_employee_rate.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
self.assertTrue(timesheet.so_line == task.sale_line_id == self.so.order_line[0], "The SOL in the timesheet should be the same than the one in the task.")
|
||||
self.assertEqual(self.so.order_line[0].qty_delivered, 1, "The quantity delivered should be equal to 1 for all SOLs in the SO.")
|
||||
|
||||
# 4) Create timesheet in the task for the employee defined in the mapping and check if the SOL in this timesheet is the one defined in the # mapping,
|
||||
employee_user_timesheet = timesheet.copy({
|
||||
'name': 'Test Line Employee User',
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': 2,
|
||||
})
|
||||
employee_user_timesheet._compute_so_line()
|
||||
self.assertTrue(employee_user_timesheet.so_line == self.project_employee_rate.sale_line_employee_ids[0].sale_line_id == self.so.order_line[1], "The SOL in the timesheet should be the one defined in the mapping for the employee user.")
|
||||
self.assertEqual(self.so.order_line[1].qty_delivered, 2, "The quantity delivered for this SOL should be equal to 2 hours.")
|
||||
|
||||
# 5) Change the SOL in the task and check if only the SOL in the timesheet which does not concerne about the mapping changes,
|
||||
task.update({'sale_line_id': self.so.order_line[2].id})
|
||||
self.assertTrue(timesheet.so_line == task.sale_line_id == self.so.order_line[2], "The SOL in the timesheet should also change and be the same than the one in the task.")
|
||||
self.assertNotEqual(timesheet.so_line, employee_user_timesheet.so_line, "The SOL in the timesheet done by the employee user should not be the same than the one in the other timesheet in the task.")
|
||||
|
||||
# 6) Change the SOL in the mapping and check if the timesheet conserne by the mapping has its SOL has been changed too.
|
||||
mapping.update({'sale_line_id': self.so.order_line[-1].id})
|
||||
self.assertTrue(employee_user_timesheet.so_line == mapping.sale_line_id == self.so.order_line[-1], "The SOL in the timesheet done by the employee user should be the one defined in the mapping.")
|
||||
self.assertNotEqual(timesheet.so_line, employee_user_timesheet.so_line, "The other timesheet should not have the SOL defined in the mapping.")
|
||||
|
||||
def test_no_so_line_if_project_non_billable(self):
|
||||
""" Test if the timesheet created in task in non billable project does not have a SOL
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Create task in a non billable project,
|
||||
2) Check if there is no SOL in task,
|
||||
3) Create timesheet in the task and check if it does not contain any SOL.
|
||||
"""
|
||||
# 1) Create task in a non billable project,
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Task',
|
||||
'project_id': self.project_non_billable.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
# 2) Check if there is no SOL in task,
|
||||
self.assertFalse(task.sale_line_id, 'No SOL should be linked in this task because the project is non billable.')
|
||||
|
||||
# 3) Create timesheet in the task and check if it does not contain any SOL.
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 1,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
self.assertFalse(timesheet.so_line, 'No SOL should be linked in this timesheet because the project is non billable.')
|
||||
|
||||
def test_tranfer_project(self):
|
||||
""" Test if the SOL in timesheet is erased if the task of this timesheet changes the project
|
||||
from a billable project to a non billable project
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Create task in project_task_rate,
|
||||
2) Check if the task has the SOL which contain the prepaid service product,
|
||||
3) Create timesheet in this task,
|
||||
4) Check if the timesheet contains the same SOL than the task,
|
||||
5) Move the task in a non billable project,
|
||||
6) Check if the task and timesheet has no SOL.
|
||||
"""
|
||||
# 1) Create task in project_task_rate,
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Task',
|
||||
'project_id': self.project_task_rate.id,
|
||||
})
|
||||
|
||||
# 2) Check if the task has the SOL which contain the prepaid service product,
|
||||
self.assertEqual(task.sale_line_id, self.so.order_line[-1], 'The SOL with prepaid service product should be linked to the task.')
|
||||
|
||||
# 3) Create timesheet in this task,
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 1,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
|
||||
# 4) Check if the timesheet contains the same SOL than the task,
|
||||
self.assertEqual(timesheet.so_line, task.sale_line_id, 'The timesheet should have the same SOL than the task.')
|
||||
|
||||
# 5) Move the task in a non billable project,
|
||||
task.write({'project_id': self.project_non_billable.id})
|
||||
|
||||
# 6) Check if the task and timesheet has no SOL.
|
||||
self.assertFalse(timesheet.so_line, 'No SOL should be linked to the timesheet because the project is non billable')
|
||||
@@ -9,6 +9,12 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_tests" name="Sales Timesheet Assets Tests" inherit_id="web.assets_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/sale_timesheet/static/tests/tours/sale_timesheet_tour.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<record id="timesheet_plan" model="ir.ui.view">
|
||||
<field name="name">Timesheet Plan</field>
|
||||
<field name="type">qweb</field>
|
||||
|
||||
@@ -4,4 +4,3 @@
|
||||
from . import project_create_sale_order
|
||||
from . import project_create_invoice
|
||||
from . import sale_make_invoice_advance
|
||||
from . import project_task_create_sale_order
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ProjectTaskCreateSalesOrder(models.TransientModel):
|
||||
_name = 'project.task.create.sale.order'
|
||||
_description = "Create SO from task"
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(ProjectTaskCreateSalesOrder, self).default_get(fields)
|
||||
|
||||
active_model = self._context.get('active_model')
|
||||
if active_model != 'project.task':
|
||||
raise UserError(_("You can only apply this action from a task."))
|
||||
|
||||
active_id = self._context.get('active_id')
|
||||
if 'task_id' in fields and active_id:
|
||||
task = self.env['project.task'].browse(active_id)
|
||||
if task.sale_order_id:
|
||||
raise UserError(_("The task has already a sale order."))
|
||||
result['task_id'] = active_id
|
||||
if not result.get('partner_id', False):
|
||||
result['partner_id'] = task.partner_id.id
|
||||
return result
|
||||
|
||||
link_selection = fields.Selection([('create', 'Create a new sales order'), ('link', 'Link to an existing sales order')], required=True, default='create')
|
||||
|
||||
task_id = fields.Many2one('project.task', "Task", domain=[('sale_line_id', '=', False)], help="Task for which we are creating a sales order", required=True)
|
||||
partner_id = fields.Many2one('res.partner', string="Customer", help="Customer of the sales order", required=True)
|
||||
product_id = fields.Many2one('product.product', domain=[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')], string="Service", help="Product of the sales order item. Must be a service invoiced based on timesheets on tasks. The existing timesheet will be linked to this product.", required=True)
|
||||
price_unit = fields.Float("Unit Price", help="Unit price of the sales order item.")
|
||||
currency_id = fields.Many2one('res.currency', string="Currency", related='product_id.currency_id', readonly=False)
|
||||
commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id')
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order', string="Sales Order",
|
||||
domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
|
||||
sale_line_id = fields.Many2one(
|
||||
'sale.order.line', 'Sales Order Item',
|
||||
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_id', '=?', sale_order_id)]")
|
||||
|
||||
info_invoice = fields.Char(compute='_compute_info_invoice')
|
||||
|
||||
@api.depends('sale_line_id', 'price_unit', 'link_selection')
|
||||
def _compute_info_invoice(self):
|
||||
for line in self:
|
||||
domain = self.env['sale.order.line']._timesheet_compute_delivered_quantity_domain()
|
||||
timesheet = self.env['account.analytic.line'].read_group(domain + [('task_id', '=', self.task_id.id), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)], ['unit_amount'], ['task_id'])
|
||||
unit_amount = round(timesheet[0].get('unit_amount', 0), 2) if timesheet else 0
|
||||
if not unit_amount:
|
||||
line.info_invoice = False
|
||||
continue
|
||||
company_uom = self.env.company.timesheet_encode_uom_id
|
||||
label = _("hours")
|
||||
if company_uom == self.env.ref('uom.product_uom_day'):
|
||||
label = _("days")
|
||||
if line.link_selection == 'create' and line.price_unit:
|
||||
line.info_invoice = _("%(amount)s %(label)s will be added to the new Sales Order.", amount=unit_amount, label=label)
|
||||
else:
|
||||
line.info_invoice = _("%(amount)s %(label)s will be added to the selected Sales Order.", amount=unit_amount, label=label)
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.price_unit = self.product_id.lst_price
|
||||
else:
|
||||
self.price_unit = 0.0
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
self.sale_order_id = False
|
||||
self.sale_line_id = False
|
||||
|
||||
@api.onchange('sale_order_id')
|
||||
def _onchange_sale_order_id(self):
|
||||
self.sale_line_id = False
|
||||
|
||||
def action_link_sale_order(self):
|
||||
# link task to SOL
|
||||
self.task_id.write({
|
||||
'sale_line_id': self.sale_line_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'email_from': self.partner_id.email,
|
||||
})
|
||||
|
||||
# assign SOL to timesheets
|
||||
self.env['account.analytic.line'].search([('task_id', '=', self.task_id.id), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)]).write({
|
||||
'so_line': self.sale_line_id.id
|
||||
})
|
||||
|
||||
def action_create_sale_order(self):
|
||||
sale_order = self._prepare_sale_order()
|
||||
sale_order.action_confirm()
|
||||
view_form_id = self.env.ref('sale.view_order_form').id
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("sale.action_orders")
|
||||
action.update({
|
||||
'views': [(view_form_id, 'form')],
|
||||
'view_mode': 'form',
|
||||
'name': sale_order.name,
|
||||
'res_id': sale_order.id,
|
||||
})
|
||||
return action
|
||||
|
||||
def _prepare_sale_order(self):
|
||||
# if task linked to SO line, then we consider it as billable.
|
||||
if self.task_id.sale_line_id:
|
||||
raise UserError(_("The task is already linked to a sales order item."))
|
||||
|
||||
# create SO
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
'company_id': self.task_id.company_id.id,
|
||||
'analytic_account_id': self.task_id.project_id.analytic_account_id.id,
|
||||
})
|
||||
sale_order.onchange_partner_id()
|
||||
sale_order.onchange_partner_shipping_id()
|
||||
# rewrite the user as the onchange_partner_id erases it
|
||||
sale_order.write({'user_id': self.task_id.user_id.id})
|
||||
sale_order.onchange_user_id()
|
||||
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': self.product_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
'project_id': self.task_id.project_id.id, # prevent to re-create a project on confirmation
|
||||
'task_id': self.task_id.id,
|
||||
'product_uom_qty': round(sum(self.task_id.timesheet_ids.filtered(lambda t: not t.so_line).mapped('unit_amount')), 2),
|
||||
})
|
||||
|
||||
# link task to SOL
|
||||
self.task_id.write({
|
||||
'sale_line_id': sale_order_line.id,
|
||||
'partner_id': sale_order.partner_id.id,
|
||||
'email_from': sale_order.partner_id.email,
|
||||
})
|
||||
|
||||
# assign SOL to timesheets
|
||||
self.env['account.analytic.line'].search([('task_id', '=', self.task_id.id), ('so_line', '=', False), ('timesheet_invoice_id', '=', False)]).write({
|
||||
'so_line': sale_order_line.id
|
||||
})
|
||||
|
||||
return sale_order
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="project_task_create_sale_order_view_form" model="ir.ui.view">
|
||||
<field name="name">project.task.create.sale.order.wizard.form</field>
|
||||
<field name="model">project.task.create.sale.order</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create a Sales Order">
|
||||
<field name="link_selection" nolabel="1" widget="radio" options="{'horizontal': true}" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="task_id" readonly="1"/>
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('link_selection', '=', 'link')]}">
|
||||
<field name="product_id" context="{'default_type': 'service', 'default_service_policy': 'delivered_timesheet'}" attrs="{'required': [('link_selection', '!=', 'link')]}"/>
|
||||
<field name="price_unit" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'required': [('link_selection', '!=', 'link')]}"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('link_selection', '!=', 'link')]}">
|
||||
<field name="sale_order_id" options="{'no_create': True, 'no_create_edit': True}" attrs="{'required': [('link_selection', '=', 'link')]}"/>
|
||||
<field name="sale_line_id" options="{'no_create': True, 'no_create_edit': True}" attrs="{'required': [('link_selection', '=', 'link')]}"/>
|
||||
<field name="commercial_partner_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('info_invoice', '=', False)]}">
|
||||
<field name="info_invoice" nolabel="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Link to Sales Order" type="object" name="action_link_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '!=', 'link')]}"/>
|
||||
<button string="Create Sales Order" type="object" name="action_create_sale_order" class="oe_highlight" attrs="{'invisible': [('link_selection', '=', 'link')]}"/>
|
||||
<button string="Cancel" special="cancel" type="object" class="btn btn-secondary oe_inline"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_action_multi_create_sale_order" model="ir.actions.act_window">
|
||||
<field name="name">Create a Sales Order</field>
|
||||
<field name="res_model">project.task.create.sale.order</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="project_task_create_sale_order_view_form"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user