mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 16:12:04 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -8,7 +8,6 @@ from . import test_project
|
|||
from . import test_project_billing
|
||||
from . import test_project_profitability
|
||||
from . import test_reinvoice
|
||||
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
|
||||
|
|
@ -16,3 +15,9 @@ from . import test_sale_timesheet_ui
|
|||
from . import test_project_pricing_type
|
||||
from . import test_project_update
|
||||
from . import test_sale_timesheet_accrued_entries
|
||||
from . import test_sale_timesheet_report
|
||||
from . import test_sale_timesheet_product_product
|
||||
from . import test_sale_timesheet_product_template
|
||||
from . import test_sale_timesheet_dashboard
|
||||
from . import test_task_analysis
|
||||
from . import test_performance
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ from odoo.addons.sale_project.tests.common import TestSaleProjectCommon
|
|||
class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
cls.env.company.resource_calendar_id.tz = "Europe/Brussels"
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.group_ids |= cls.env.ref('hr.group_hr_user')
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
cls.user_employee_company_B = mail_new_test_user(
|
||||
cls.env,
|
||||
|
|
@ -32,34 +33,43 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
company_id=cls.company_data_2['company'].id,
|
||||
company_ids=[cls.company_data_2['company'].id, cls.env.company.id],
|
||||
)
|
||||
# What's important here is that this user does not have access to read Sales data,
|
||||
# but can still log time on a timesheet.
|
||||
cls.user_employee_without_sales_access = mail_new_test_user(
|
||||
cls.env,
|
||||
name='Tyrion Lannister Employee',
|
||||
login='tyrion',
|
||||
email='tyrion@example.com',
|
||||
notification_type='email',
|
||||
groups='project.group_project_manager,hr_timesheet.group_hr_timesheet_user',
|
||||
)
|
||||
|
||||
cls.employee_user = cls.env['hr.employee'].create({
|
||||
'name': 'Employee User',
|
||||
'hourly_cost': 15,
|
||||
})
|
||||
cls.employee_manager = cls.env['hr.employee'].create({
|
||||
'name': 'Employee Manager',
|
||||
'hourly_cost': 45,
|
||||
})
|
||||
|
||||
cls.employee_company_B = cls.env['hr.employee'].create({
|
||||
'name': 'Gregor Clegane',
|
||||
'user_id': cls.user_employee_company_B.id,
|
||||
'hourly_cost': 15,
|
||||
})
|
||||
|
||||
cls.manager_company_B = cls.env['hr.employee'].create({
|
||||
'name': 'Cersei Lannister',
|
||||
'user_id': cls.user_manager_company_B.id,
|
||||
'hourly_cost': 45,
|
||||
})
|
||||
cls.employee_user, cls.employee_manager, \
|
||||
cls.employee_company_B, cls.manager_company_B, \
|
||||
cls.employee_without_sales_access = \
|
||||
cls.env['hr.employee'].create([{
|
||||
'name': 'Employee User',
|
||||
'hourly_cost': 15,
|
||||
}, {
|
||||
'name': 'Employee Manager',
|
||||
'hourly_cost': 45,
|
||||
}, {
|
||||
'name': 'Gregor Clegane',
|
||||
'user_id': cls.user_employee_company_B.id,
|
||||
'hourly_cost': 15,
|
||||
}, {
|
||||
'name': 'Cersei Lannister',
|
||||
'user_id': cls.user_manager_company_B.id,
|
||||
'hourly_cost': 45,
|
||||
}, {
|
||||
'name': 'Tyrion Lannister',
|
||||
'user_id': cls.user_employee_without_sales_access.id,
|
||||
'hourly_cost': 25,
|
||||
}])
|
||||
|
||||
# Account and project
|
||||
cls.analytic_account_sale.name = 'Project for selling timesheet - AA'
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan Test',
|
||||
'company_id': cls.company_data_2['company'].id,
|
||||
})
|
||||
cls.analytic_plan, _other_plans = cls.env['account.analytic.plan']._get_all_plans()
|
||||
cls.analytic_account_sale_company_B = cls.env['account.analytic.account'].create({
|
||||
'name': 'Project for selling timesheet Company B - AA',
|
||||
'code': 'AA-2030',
|
||||
|
|
@ -68,7 +78,7 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
})
|
||||
|
||||
# Create projects
|
||||
Project = cls.env['project.project'].with_context(tracking_disable=True)
|
||||
Project = cls.env['project.project']
|
||||
cls.project_global.write({
|
||||
'name': 'Project for selling timesheets',
|
||||
'allow_timesheets': True,
|
||||
|
|
@ -82,7 +92,7 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'partner_id': cls.partner_b.id,
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
'account_id': cls.analytic_account_sale.id,
|
||||
})
|
||||
|
||||
cls.project_subtask = Project.create({
|
||||
|
|
@ -108,7 +118,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED1',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'no',
|
||||
|
|
@ -123,7 +132,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED2',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'task_global_project',
|
||||
|
|
@ -138,7 +146,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED3',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'task_in_project',
|
||||
|
|
@ -153,7 +160,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED4',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'project_only',
|
||||
|
|
@ -168,7 +174,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED4',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'project_only',
|
||||
|
|
@ -186,7 +191,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI1',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'no',
|
||||
|
|
@ -201,7 +205,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI2',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'task_global_project',
|
||||
|
|
@ -216,7 +219,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI3',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'task_in_project',
|
||||
|
|
@ -231,7 +233,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI4',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'project_only',
|
||||
|
|
@ -246,7 +247,6 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI5',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'project_only',
|
||||
|
|
@ -255,10 +255,20 @@ class TestCommonSaleTimesheet(TestSaleProjectCommon):
|
|||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
cls.product_service_delivered_timesheet = cls.env['product.product'].create({
|
||||
'name': "Service timesheet",
|
||||
'standard_price': 11,
|
||||
'list_price': 13,
|
||||
'type': 'service',
|
||||
'service_tracking': 'no',
|
||||
'project_id': False,
|
||||
'invoice_policy': 'delivery',
|
||||
'service_type': 'timesheet',
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.so = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
self.so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_b.id,
|
||||
'partner_invoice_id': self.partner_b.id,
|
||||
'partner_shipping_id': self.partner_b.id,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class TestEditSoLineTimesheet(TestCommonSaleTimesheet):
|
|||
# 1) create some timesheets on this task
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'auto_account_id': self.analytic_account_sale.id,
|
||||
'project_id': self.project_task_rate.id,
|
||||
'task_id': self.task_rate_task.id,
|
||||
'unit_amount': 5,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale_timesheet.tests.test_sale_timesheet import TestSaleTimesheet
|
||||
from odoo.tests import tagged
|
||||
from odoo.fields import Command
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestPerformanceTimesheet(TestSaleTimesheet):
|
||||
|
||||
def test_performance_billable_project_change_customer(self):
|
||||
"""
|
||||
Use case: change the partner of a billable project containing many tasks having no SOL, which should trigger _compute_sale_line_id() of all tasks.
|
||||
We check if the number of queries does not increase proportionally to the number of tasks.
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Perf Project',
|
||||
'task_ids': [Command.create({'name': f'Task {i}'}) for i in range(50)]
|
||||
})
|
||||
self.assertFalse(project.task_ids.sale_line_id)
|
||||
self.env.invalidate_all()
|
||||
with self.assertQueryCount(85):
|
||||
project.write({
|
||||
'allow_billable': True,
|
||||
'partner_id': self.partner_b.id,
|
||||
})
|
||||
self.assertTrue(project.task_ids.sale_line_id)
|
||||
|
||||
# Reset all tasks's SOL to False, double the number of tasks and run it again
|
||||
project.allow_billable = False
|
||||
self.assertFalse(project.task_ids.sale_line_id)
|
||||
self.env['project.task'].create([{
|
||||
'name': f'Task {i}',
|
||||
'project_id': project.id,
|
||||
} for i in range(50, 100)])
|
||||
self.env.invalidate_all()
|
||||
with self.assertQueryCount(130):
|
||||
project.write({
|
||||
'allow_billable': True,
|
||||
'partner_id': self.partner_b.id,
|
||||
})
|
||||
self.assertTrue(project.task_ids.sale_line_id)
|
||||
|
|
@ -144,7 +144,6 @@ class TestProject(TestCommonSaleTimesheet):
|
|||
'name': self.product_delivery_timesheet1.name,
|
||||
'product_id': self.product_delivery_timesheet1.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.product_delivery_timesheet1.uom_id.id,
|
||||
'price_unit': self.product_delivery_timesheet1.list_price,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
|
@ -165,7 +164,7 @@ class TestProject(TestCommonSaleTimesheet):
|
|||
self.env['project.task'].create({
|
||||
'name': 'task A',
|
||||
'project_id': self.project_global.id,
|
||||
'planned_hours': 10,
|
||||
'allocated_hours': 10,
|
||||
'timesheet_ids': [
|
||||
Command.create({
|
||||
'name': '/',
|
||||
|
|
@ -175,12 +174,39 @@ class TestProject(TestCommonSaleTimesheet):
|
|||
],
|
||||
})
|
||||
|
||||
self.project_global.invalidate_recordset()
|
||||
self.project_global.account_id.invalidate_recordset()
|
||||
self.assertEqual(self.project_global.analytic_account_balance, expected_analytic_account_balance)
|
||||
|
||||
def test_open_product_form_with_default_service_policy(self):
|
||||
form = Form(self.env['product.product'].with_context(default_detailed_type='service', default_service_policy='delivered_timesheet'))
|
||||
form = Form(self.env['product.product'].with_context(
|
||||
default_type='service',
|
||||
default_service_policy='delivered_timesheet',
|
||||
))
|
||||
self.assertEqual('delivered_timesheet', form.service_policy)
|
||||
|
||||
def test_open_product_form_with_default_uom_id(self):
|
||||
""" Test default product uom fallback when product is not service type """
|
||||
uom_dozen = self.env.ref('uom.product_uom_dozen')
|
||||
product_form = Form(self.env['product.product'].with_context(
|
||||
default_uom_id=uom_dozen.id,
|
||||
))
|
||||
self.assertEqual(uom_dozen, product_form.uom_id, "Default uom should be Dozen")
|
||||
product_form.type = 'service'
|
||||
product_form.service_policy = 'delivered_timesheet'
|
||||
uom_hour = self.env.ref('uom.product_uom_hour')
|
||||
self.assertEqual(
|
||||
uom_hour,
|
||||
product_form.uom_id,
|
||||
"Uom should be updated to Hour for service_type=`timesheet` product"
|
||||
)
|
||||
product_form.type = 'consu'
|
||||
self.assertEqual(
|
||||
uom_dozen,
|
||||
product_form.uom_id,
|
||||
"Uom should be updated to Dozen for `Goods` type product"
|
||||
)
|
||||
|
||||
def test_duplicate_project_allocated_hours(self):
|
||||
self.project_global.allocated_hours = 10
|
||||
self.assertEqual(self.project_global.copy().allocated_hours, 10)
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from odoo.fields import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import Form
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestProjectBilling(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)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# set up
|
||||
cls.employee_tde = cls.env['hr.employee'].create({
|
||||
|
|
@ -27,8 +27,8 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
})
|
||||
|
||||
# Sale Order 1, no project/task created, used to timesheet at employee rate
|
||||
SaleOrder = cls.env['sale.order'].with_context(tracking_disable=True)
|
||||
SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True)
|
||||
SaleOrder = cls.env['sale.order']
|
||||
SaleOrderLine = cls.env['sale.order.line']
|
||||
cls.sale_order_1 = SaleOrder.create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
|
|
@ -88,149 +88,17 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
'sale_line_id': cls.so1_line_deliver_no_task.id,
|
||||
'employee_id': cls.employee_user.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)
|
||||
|
||||
def test_make_billable_at_task_rate(self):
|
||||
""" Starting from a non billable project, make it billable at task rate """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
Task = self.env['project.task']
|
||||
# set a customer on the project
|
||||
self.project_non_billable.write({
|
||||
'partner_id': self.partner_2.id,
|
||||
'timesheet_product_id': self.product_delivery_timesheet3,
|
||||
})
|
||||
# create a task and 2 timesheets
|
||||
task = Task.with_context(default_project_id=self.project_non_billable.id).create({
|
||||
'name': 'first task',
|
||||
'partner_id': self.project_non_billable.partner_id.id,
|
||||
'planned_hours': 10,
|
||||
})
|
||||
timesheet1 = Timesheet.create({
|
||||
'name': 'Test Line',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 3,
|
||||
'employee_id': self.employee_manager.id,
|
||||
})
|
||||
timesheet2 = Timesheet.create({
|
||||
'name': 'Test Line tde',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 2,
|
||||
'employee_id': self.employee_tde.id,
|
||||
})
|
||||
|
||||
# Change project to billable at task rate
|
||||
self.project_non_billable.write({
|
||||
'allow_billable': True,
|
||||
})
|
||||
|
||||
# create wizard
|
||||
wizard = self.env['project.create.sale.order'].with_context(active_id=self.project_non_billable.id, active_model='project.project').create({
|
||||
'line_ids': [
|
||||
Command.create({'product_id': self.product_delivery_timesheet3.id, 'price_unit': self.product_delivery_timesheet3.lst_price}),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertEqual(wizard.partner_id, self.project_non_billable.partner_id, "The wizard should have the same partner as the project")
|
||||
self.assertEqual(len(wizard.line_ids), 1, "The wizard should have one line")
|
||||
self.assertEqual(wizard.line_ids.product_id, self.product_delivery_timesheet3, "The wizard should have one line with right product")
|
||||
|
||||
# create the SO from the project
|
||||
action = wizard.action_create_sale_order()
|
||||
sale_order = self.env['sale.order'].browse(action['res_id'])
|
||||
|
||||
self.assertEqual(sale_order.partner_id, self.project_non_billable.partner_id, "The customer of the SO should be the same as the project")
|
||||
self.assertEqual(len(sale_order.order_line), 1, "The SO should have 1 line")
|
||||
self.assertEqual(sale_order.order_line.product_id, wizard.line_ids.product_id, "The product of the only SOL should be the selected on the wizard")
|
||||
self.assertEqual(sale_order.order_line.project_id, self.project_non_billable, "SOL should be linked to the project")
|
||||
self.assertTrue(sale_order.order_line.task_id, "The SOL creates a task as they were no task already present in the project (system limitation)")
|
||||
self.assertEqual(sale_order.order_line.task_id.project_id, self.project_non_billable, "The created task should be in the project")
|
||||
self.assertEqual(sale_order.order_line.qty_delivered, timesheet1.unit_amount + timesheet2.unit_amount, "The create SOL should have an delivered quantity equals to the sum of tasks'timesheets")
|
||||
self.assertEqual(self.project_non_billable.pricing_type, 'fixed_rate', 'The pricing type of the project should be project rate since we linked a SO in the project.')
|
||||
|
||||
def test_make_billable_at_employee_rate(self):
|
||||
""" Starting from a non billable project, make it billable at employee rate """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
Task = self.env['project.task']
|
||||
# set a customer on the project
|
||||
self.project_non_billable.write({
|
||||
'partner_id': self.partner_2.id
|
||||
})
|
||||
# create a task and 2 timesheets
|
||||
task = Task.with_context(default_project_id=self.project_non_billable.id).create({
|
||||
'name': 'first task',
|
||||
'partner_id': self.project_non_billable.partner_id.id,
|
||||
'planned_hours': 10,
|
||||
})
|
||||
timesheet1 = Timesheet.create({
|
||||
'name': 'Test Line',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 3,
|
||||
'employee_id': self.employee_manager.id,
|
||||
})
|
||||
timesheet2 = Timesheet.create({
|
||||
'name': 'Test Line tde',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 2,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
|
||||
# Change project to billable at employee rate
|
||||
self.project_non_billable.write({
|
||||
'allow_billable': True,
|
||||
})
|
||||
|
||||
# create wizard
|
||||
wizard = self.env['project.create.sale.order'].with_context(active_id=self.project_non_billable.id, active_model='project.project').create({
|
||||
'partner_id': self.partner_2.id,
|
||||
'line_ids': [
|
||||
(0, 0, {'product_id': self.product_delivery_timesheet1.id, 'price_unit': 15, 'employee_id': self.employee_tde.id}), # product creates no T
|
||||
(0, 0, {'product_id': self.product_delivery_timesheet1.id, 'price_unit': 15, 'employee_id': self.employee_manager.id}), # product creates no T (same product than previous one)
|
||||
(0, 0, {'product_id': self.product_delivery_timesheet3.id, 'price_unit': self.product_delivery_timesheet3.list_price, 'employee_id': self.employee_user.id}), # product creates new T in new P
|
||||
]
|
||||
})
|
||||
|
||||
self.assertEqual(wizard.partner_id, self.project_non_billable.partner_id, "The wizard should have the same partner as the project")
|
||||
self.assertEqual(wizard.project_id, self.project_non_billable, "The wizard'project should be the non billable project")
|
||||
|
||||
# create the SO from the project
|
||||
action = wizard.action_create_sale_order()
|
||||
sale_order = self.env['sale.order'].browse(action['res_id'])
|
||||
|
||||
self.assertEqual(sale_order.partner_id, self.project_non_billable.partner_id, "The customer of the SO should be the same as the project")
|
||||
self.assertEqual(len(sale_order.order_line), 2, "The SO should have 2 lines, as in wizard map there were 2 time the same product with the same price (for 2 different employees)")
|
||||
self.assertEqual(len(self.project_non_billable.sale_line_employee_ids), 3, "The project have 3 lines in its map")
|
||||
self.assertEqual(self.project_non_billable.pricing_type, 'employee_rate', 'The pricing type of the project should be employee rate since we have some mappings in this project.')
|
||||
self.assertEqual(self.project_non_billable.sale_line_id, sale_order.order_line[0], "The wizard sets sale line fallbakc on project as the first of the list")
|
||||
self.assertEqual(task.sale_line_id, sale_order.order_line[0], "The wizard sets sale line fallback on tasks")
|
||||
self.assertEqual(task.partner_id, wizard.partner_id, "The wizard sets the customer on tasks to make SOL line field visible")
|
||||
|
||||
line1 = sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_delivery_timesheet1)
|
||||
line2 = sale_order.order_line.filtered(lambda sol: sol.product_id == self.product_delivery_timesheet3)
|
||||
|
||||
self.assertTrue(line1, "Sale line 1 with product 1 should exists")
|
||||
self.assertTrue(line2, "Sale line 2 with product 3 should exists")
|
||||
|
||||
self.assertFalse(line1.project_id, "Sale line 1 should be linked to the 'non billable' project")
|
||||
self.assertEqual(line2.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project")
|
||||
self.assertEqual(line1.price_unit, 15.0, "The unit price of SOL 1 should be 15.0")
|
||||
self.assertEqual(line1.product_uom_qty, 3, "The ordered qty of SOL 1 should be 3")
|
||||
self.assertEqual(line2.product_uom_qty, 2, "The ordered qty of SOL 2 should be 2")
|
||||
|
||||
self.assertEqual(self.project_non_billable.sale_line_employee_ids.mapped('sale_line_id'), sale_order.order_line, "The SO lines of the map should be the same of the sales order")
|
||||
self.assertEqual(timesheet1.so_line, line1, "Timesheet1 should be linked to sale line 1, as employee manager create the timesheet")
|
||||
self.assertEqual(timesheet2.so_line, line2, "Timesheet2 should be linked to sale line 2, as employee tde create the timesheet")
|
||||
self.assertEqual(timesheet1.unit_amount, line1.qty_delivered, "Sale line 1 should have a delivered qty equals to the sum of its linked timesheets")
|
||||
self.assertEqual(timesheet2.unit_amount, line2.qty_delivered, "Sale line 2 should have a delivered qty equals to the sum of its linked timesheets")
|
||||
cls.project_sale_manager = mail_new_test_user(
|
||||
cls.env,
|
||||
name='Project Manager',
|
||||
login='project_manager',
|
||||
email='project_manager@example.com',
|
||||
groups='project.group_project_manager,sales_team.group_sale_manager',
|
||||
)
|
||||
|
||||
def test_billing_employee_rate(self):
|
||||
""" Check task and subtask creation, and timesheeting in a project billed at 'employee rate'. Then move the task into a 'task rate' project. """
|
||||
Task = self.env['project.task'].with_context(tracking_disable=True)
|
||||
Task = self.env['project.task']
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
|
||||
# create a task
|
||||
|
|
@ -265,14 +133,15 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
self.assertEqual(self.project_employee_rate_manager.project_id, timesheet1.project_id, "The timesheet should be linked to the project of the map entry")
|
||||
|
||||
# create a subtask
|
||||
subtask = Task.with_context(default_project_id=self.project_subtask.id).create({
|
||||
subtask = Task.create({
|
||||
'name': 'first subtask task',
|
||||
'parent_id': task.id,
|
||||
'project_id': self.project_subtask.id,
|
||||
})
|
||||
|
||||
self.assertFalse(subtask.allow_billable, "Subtask in non billable project should be non billable too")
|
||||
self.assertFalse(subtask.project_id.allow_billable, "The subtask project is non billable even if the subtask is")
|
||||
self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask should have the same customer as the one from their mother")
|
||||
self.assertFalse(subtask.partner_id, "Subtask in non billable project should not have a customer")
|
||||
|
||||
# log timesheet on subtask
|
||||
timesheet2 = Timesheet.create({
|
||||
|
|
@ -296,12 +165,12 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
self.assertEqual(task.sale_line_id, self.so1_line_deliver_no_task, "The task should keep the same SOL since the partner_id has not changed when the project of the task has changed.")
|
||||
self.assertEqual(task.partner_id, self.partner_a, "Task created in a project billed on 'employee rate' should have the same customer when it has been created.")
|
||||
# the `subtask.sale_line_id` is consider to be recompute,
|
||||
# but the result differ after the write of display_project_id without depend on it
|
||||
# but the result differ after the write of project_id without depend on it
|
||||
task.flush_model(["sale_line_id"])
|
||||
|
||||
# move subtask into task rate project
|
||||
subtask.write({
|
||||
'display_project_id': self.project_task_rate2.id,
|
||||
'project_id': self.project_task_rate2.id,
|
||||
})
|
||||
|
||||
self.assertTrue(subtask.allow_billable, "Subtask should keep the billable type from its parent, even when they are moved into another project")
|
||||
|
|
@ -311,8 +180,9 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
task2 = Task.with_context(default_project_id=self.project_employee_rate.id).create({
|
||||
'name': 'first task',
|
||||
'partner_id': self.partner_a.id,
|
||||
'sale_line_id': False
|
||||
})
|
||||
# This needs to be done after creation because setting partner_id causes _get_last_sol_of_customer to recompute the sale_line_id
|
||||
task2.update({'sale_line_id': False})
|
||||
|
||||
# log timesheet on task in 'employee rate' project without any fallback (no map, no SOL on task, no SOL on project)
|
||||
timesheet3 = Timesheet.create({
|
||||
|
|
@ -341,7 +211,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
Check task and subtask creation, and timesheeting in a project billed at 'task rate'.
|
||||
Then move the task into a 'employee rate' project then, 'non billable'.
|
||||
"""
|
||||
Task = self.env['project.task'].with_context(tracking_disable=True)
|
||||
Task = self.env['project.task']
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
|
||||
# create a task
|
||||
|
|
@ -367,20 +237,20 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
subtask = Task.with_context(default_project_id=self.project_task_rate.id).create({
|
||||
'name': 'first subtask task',
|
||||
'parent_id': task.id,
|
||||
'display_project_id': self.project_subtask.id,
|
||||
'project_id': self.project_subtask.id,
|
||||
})
|
||||
|
||||
self.assertEqual(subtask.partner_id, subtask.parent_id.partner_id, "Subtask should have the same customer as the one from their mother")
|
||||
self.assertFalse(subtask.partner_id, "Subtask should not have the customer if it's project is not billable")
|
||||
|
||||
# log timesheet on subtask
|
||||
timesheet2 = Timesheet.create({
|
||||
'name': 'Test Line on subtask',
|
||||
'project_id': subtask.display_project_id.id,
|
||||
'project_id': subtask.project_id.id,
|
||||
'task_id': subtask.id,
|
||||
'unit_amount': 50,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(subtask.display_project_id, timesheet2.project_id, "The timesheet is in the subtask project")
|
||||
self.assertEqual(subtask.project_id, timesheet2.project_id, "The timesheet is in the subtask project")
|
||||
self.assertFalse(timesheet2.so_line, "The timesheet should not be linked to SOL as it's a non billable project")
|
||||
# the `subtask.sale_line_id` is consider to be recompute,
|
||||
# but the result differ after the write of project_id
|
||||
|
|
@ -391,7 +261,7 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
'project_id': self.project_employee_rate.id,
|
||||
})
|
||||
subtask.write({
|
||||
'display_project_id': self.project_employee_rate.id,
|
||||
'project_id': self.project_employee_rate.id,
|
||||
})
|
||||
|
||||
self.assertEqual(task.sale_line_id, self.project_task_rate.sale_line_id, "Task moved in a employee rate billable project should keep its SOL because the partner_id has not changed too.")
|
||||
|
|
@ -439,15 +309,15 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
3) Create an employee mapping in this project
|
||||
4) Check if the partner_id and pricing_type fields have been changed
|
||||
"""
|
||||
with Form(self.env['project.project'].with_context({'tracking_disable': True})) as project_form:
|
||||
with Form(self.env['project.project'].with_user(self.project_sale_manager)) as project_form:
|
||||
project_form.name = 'Test Billable Project'
|
||||
project_form.allow_billable = True
|
||||
# `sale_line_employee_ids` is not visible if `partner_id` is not set
|
||||
# As the behavior of the test is to check the partner on the project
|
||||
# is set to the partner of the order line, temporary make the field visible
|
||||
# even if it's not the case in the reality, in the web client
|
||||
# {'invisible': ['|', ('allow_billable', '=', False), ('partner_id', '=', False)]}
|
||||
project_form._view['modifiers']['sale_line_employee_ids']['invisible'] = False
|
||||
# not allow_billable or not partner_id
|
||||
project_form._view['modifiers']['sale_line_employee_ids']['invisible'] = 'False'
|
||||
with project_form.sale_line_employee_ids.new() as mapping_form:
|
||||
mapping_form.employee_id = self.employee_manager
|
||||
mapping_form.sale_line_id = self.so.order_line[:1]
|
||||
|
|
@ -463,7 +333,6 @@ class TestProjectBilling(TestCommonSaleTimesheet):
|
|||
this test will manually set the state and payment_status to be in the same condition
|
||||
than the feature "Invoicing Switch Threshold".
|
||||
"""
|
||||
self.sale_order_1.action_confirm()
|
||||
timesheet1 = self.env['account.analytic.line'].create({
|
||||
'name': '/',
|
||||
'project_id': self.project_task_rate.id,
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
# -*- 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.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectBillingMulticompany(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
Project = cls.env['project.project'].with_context(tracking_disable=True)
|
||||
cls.project_non_billable = Project.create({
|
||||
'name': "Non Billable Project",
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
|
||||
def test_makeBillable_multiCompany(self):
|
||||
wizard = self.env['project.create.sale.order'].with_context(allowed_company_ids=[self.company_data_2['company'].id, self.env.company.id], company_id=self.company_data_2['company'].id, active_id=self.project_non_billable.id, active_model='project.project').create({
|
||||
'line_ids': [(0, 0, {
|
||||
'product_id': self.product_delivery_timesheet3.id, # product creates new Timesheet in new Project
|
||||
'price_unit': self.product_delivery_timesheet3.list_price
|
||||
})],
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
|
||||
action = wizard.action_create_sale_order()
|
||||
sale_order = self.env['sale.order'].browse(action['res_id'])
|
||||
|
||||
self.assertEqual(sale_order.company_id.id, self.project_non_billable.company_id.id, "The company on the sale order should be the same as the one on the project")
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.fields import Domain
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import TestCommonSaleTimesheet
|
||||
|
|
@ -19,18 +20,23 @@ class TestProjectPricingType(TestCommonSaleTimesheet):
|
|||
3) Set a customer and a SOL in the project and check if the pricing_type is equal to fixed_rate (project rate)
|
||||
4) Set a employee mapping and check if the pricing_type is equal to employee_rate
|
||||
"""
|
||||
# 1) Take a project non billable and check if the pricing_type is equal to False
|
||||
project = self.project_non_billable
|
||||
|
||||
def _search_pricing_type(operator, value):
|
||||
# execute the optimization to transform the domain
|
||||
return Domain('pricing_type', operator, value).optimize_full(project)
|
||||
|
||||
# 1) Take a project non billable and check if the pricing_type is equal to False
|
||||
self.assertFalse(project.allow_billable, 'The allow_billable should be false if the project is non billable.')
|
||||
self.assertFalse(project.pricing_type, 'The pricing type of this project should be equal to False since it is non billable.')
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('!=', False)))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('!=', False)))
|
||||
|
||||
# 2) Set allow_billable to True and check if the pricing_type is equal to task_rate (if no SOL and no mappings)
|
||||
project.write({
|
||||
|
|
@ -42,14 +48,14 @@ class TestProjectPricingType(TestCommonSaleTimesheet):
|
|||
self.assertFalse(project.sale_line_id, 'The sales order item should be unset.')
|
||||
self.assertFalse(project.sale_line_employee_ids, 'The employee mappings should be empty.')
|
||||
self.assertEqual(project.pricing_type, 'task_rate', 'The pricing type should be equal to task_rate.')
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', False)))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', False)))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', False)))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', False)))
|
||||
|
||||
# 3) Set a customer and a SOL in the project and check if the pricing_type is equal to fixed_rate (project rate)
|
||||
project.write({
|
||||
|
|
@ -60,14 +66,14 @@ class TestProjectPricingType(TestCommonSaleTimesheet):
|
|||
self.assertEqual(project.sale_order_id, self.so, 'The sales order should be equal to the one set in the project.')
|
||||
self.assertEqual(project.sale_line_id, self.so.order_line[0], 'The sales order item should be the one chosen.')
|
||||
self.assertEqual(project.pricing_type, 'fixed_rate', 'The pricing type should be equal to fixed_rate since the project has a sales order item.')
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', False)))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', False)))
|
||||
|
||||
# 4) Set a employee mapping and check if the pricing_type is equal to employee_rate
|
||||
project.write({
|
||||
|
|
@ -79,14 +85,14 @@ class TestProjectPricingType(TestCommonSaleTimesheet):
|
|||
|
||||
self.assertEqual(len(project.sale_line_employee_ids), 1, 'The project should have an employee mapping.')
|
||||
self.assertEqual(project.pricing_type, 'employee_rate', 'The pricing type should be equal to employee_rate since the project has an employee mapping.')
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', False)))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', False)))
|
||||
|
||||
# Even if the project has no sales order item, since it has an employee mapping, the pricing type must be equal to employee_rate.
|
||||
project.write({
|
||||
|
|
@ -95,11 +101,11 @@ class TestProjectPricingType(TestCommonSaleTimesheet):
|
|||
self.assertFalse(project.sale_order_id, 'The sales order of the project should be empty.')
|
||||
self.assertFalse(project.sale_line_id, 'The sales order item of the project should be empty.')
|
||||
self.assertEqual(project.pricing_type, 'employee_rate', 'The pricing type should always be equal to employee_rate.')
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(project._search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(project._search_pricing_type('!=', False)))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'task_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', 'fixed_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('=', 'employee_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('=', False)))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'task_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', 'fixed_rate')))
|
||||
self.assertFalse(project.filtered_domain(_search_pricing_type('!=', 'employee_rate')))
|
||||
self.assertTrue(project.filtered_domain(_search_pricing_type('!=', False)))
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ from .common import TestCommonSaleTimesheet
|
|||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.task = cls.env['project.task'].create({
|
||||
'name': 'Test',
|
||||
'project_id': cls.project_task_rate.id,
|
||||
|
|
@ -19,33 +19,37 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet):
|
|||
'revenues': {'data': [], 'total': {'to_invoice': 0.0, 'invoiced': 0.0}},
|
||||
'costs': {'data': [], 'total': {'to_bill': 0.0, 'billed': 0.0}},
|
||||
}
|
||||
|
||||
def test_profitability_of_non_billable_project(self):
|
||||
""" Test no data is found for the project profitability since the project is not billable
|
||||
even if it is linked to a sale order items.
|
||||
"""
|
||||
self.assertFalse(self.project_non_billable.allow_billable)
|
||||
self.assertDictEqual(
|
||||
self.project_non_billable._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
)
|
||||
self.project_non_billable.write({'sale_line_id': self.so.order_line[0].id})
|
||||
self.assertDictEqual(
|
||||
self.project_non_billable._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
"Even if the project has a sale order item linked the project profitability should not be computed since it is not billable."
|
||||
)
|
||||
cls.foreign_currency = cls.env['res.currency'].create({
|
||||
'name': 'Chaos orb',
|
||||
'symbol': '☺',
|
||||
'rounding': 0.001,
|
||||
'position': 'after',
|
||||
'currency_unit_label': 'Chaos',
|
||||
'currency_subunit_label': 'orb',
|
||||
})
|
||||
cls.env['res.currency.rate'].create({
|
||||
'name': '2016-01-01',
|
||||
'rate': '5.0',
|
||||
'currency_id': cls.foreign_currency.id,
|
||||
'company_id': cls.env.company.id,
|
||||
})
|
||||
|
||||
def test_get_project_profitability_items(self):
|
||||
""" Test _get_project_profitability_items method to ensure the project profitability
|
||||
is correctly computed as expected.
|
||||
is computed as expected.
|
||||
"""
|
||||
sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
foreign_company = self.company_data_2['company']
|
||||
foreign_company.currency_id = self.foreign_currency
|
||||
self.project_task_rate.account_id.company_id = False
|
||||
self.project_task_rate.company_id = False
|
||||
|
||||
# Create and confirm a SO with the main company
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_b.id,
|
||||
'partner_invoice_id': self.partner_b.id,
|
||||
'partner_shipping_id': self.partner_b.id,
|
||||
})
|
||||
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True, default_order_id=sale_order.id)
|
||||
SaleOrderLine = self.env['sale.order.line'].with_context(default_order_id=sale_order.id)
|
||||
delivery_service_order_line = SaleOrderLine.create({
|
||||
'product_id': self.product_delivery_manual1.id,
|
||||
'product_uom_qty': 5,
|
||||
|
|
@ -58,9 +62,102 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet):
|
|||
'No timesheets has been recorded in the task and no product has been deelivered in the SO linked so the project profitability has no data found.'
|
||||
)
|
||||
|
||||
# Create and confirm a SO with the foreign company
|
||||
sale_order_foreign = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_b.id,
|
||||
'partner_invoice_id': self.partner_b.id,
|
||||
'partner_shipping_id': self.partner_b.id,
|
||||
'company_id': foreign_company.id,
|
||||
})
|
||||
sale_order_foreign.currency_id = foreign_company.currency_id
|
||||
SaleOrderLineForeign = self.env['sale.order.line'].with_context(default_order_id=sale_order_foreign.id)
|
||||
SaleOrderLineForeign.create({
|
||||
'product_id': self.product_delivery_manual1.id,
|
||||
'product_uom_qty': 5,
|
||||
})
|
||||
sale_order_foreign.action_confirm()
|
||||
self.task.write({'sale_line_id': delivery_service_order_line.id})
|
||||
# Create the foreign users needed for the foreign timesheets
|
||||
foreign_partner = self.env['res.partner'].create({
|
||||
'name': 'Foreign Employee address',
|
||||
'company_id': foreign_company.id,
|
||||
})
|
||||
foreign_employee = self.env['hr.employee'].create({
|
||||
'name': 'test',
|
||||
'company_id': foreign_company.id,
|
||||
'work_contact_id': foreign_partner.id,
|
||||
'hourly_cost': 200,
|
||||
})
|
||||
foreign_employee_2 = self.env['hr.employee'].create({
|
||||
'name': 'test',
|
||||
'company_id': foreign_company.id,
|
||||
'work_contact_id': foreign_partner.id,
|
||||
'hourly_cost': 500,
|
||||
})
|
||||
# Create 2 new timesheets linked to the task of the project
|
||||
Timesheet = self.env['account.analytic.line'].with_context(
|
||||
default_task_id=self.task.id,
|
||||
)
|
||||
foreign_timesheet1 = Timesheet.create({
|
||||
'name': 'Foreign Timesheet 1',
|
||||
'employee_id': foreign_employee.id,
|
||||
'project_id': self.project_task_rate.id,
|
||||
'unit_amount': 3.0,
|
||||
'company_id': foreign_company.id,
|
||||
})
|
||||
foreign_timesheet2 = Timesheet.create({
|
||||
'name': 'Foreign Timesheet 2',
|
||||
'employee_id': foreign_employee.id,
|
||||
'project_id': self.project_task_rate.id,
|
||||
'unit_amount': 2.0,
|
||||
'company_id': foreign_company.id,
|
||||
})
|
||||
|
||||
sequence_per_invoice_type = self.project_task_rate._get_profitability_sequence_per_invoice_type()
|
||||
self.assertIn('billable_time', sequence_per_invoice_type)
|
||||
self.assertIn('billable_fixed', sequence_per_invoice_type)
|
||||
self.assertIn('billable_milestones', sequence_per_invoice_type)
|
||||
self.assertIn('billable_manual', sequence_per_invoice_type)
|
||||
self.assertEqual(self.task.sale_line_id, delivery_service_order_line)
|
||||
self.assertEqual((foreign_timesheet1 + foreign_timesheet2).so_line, delivery_service_order_line)
|
||||
self.assertEqual(delivery_service_order_line.qty_delivered, 0.0, 'The service type is not timesheet but manual so the quantity delivered is not increased by the timesheets linked.')
|
||||
|
||||
# Adding an extra cost/revenue to ensure those are computed correctly.
|
||||
self.env['account.analytic.line'].create([{
|
||||
'name': 'other revenues line',
|
||||
'account_id': self.project_task_rate.account_id.id,
|
||||
'amount': 100,
|
||||
}, {
|
||||
'name': 'other costs line',
|
||||
'account_id': self.project_task_rate.account_id.id,
|
||||
'amount': -100,
|
||||
}])
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [{'id': 'other_revenues_aal', 'sequence': sequence_per_invoice_type['other_revenues_aal'], 'invoiced': 100.0, 'to_invoice': 0.0}],
|
||||
'total': {'invoiced': 100.0, 'to_invoice': 0.0},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_costs_aal', 'sequence': sequence_per_invoice_type['other_costs_aal'], 'billed': -100.0, 'to_bill': 0.0},
|
||||
{
|
||||
'id': 'billable_manual',
|
||||
'sequence': sequence_per_invoice_type['billable_manual'],
|
||||
'billed': (foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': -100 + (foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Create 2 new timesheets linked to the task of the project
|
||||
timesheet1 = Timesheet.create({
|
||||
'name': 'Timesheet 1',
|
||||
'employee_id': self.employee_user.id,
|
||||
|
|
@ -73,40 +170,82 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet):
|
|||
'project_id': self.project_task_rate.id,
|
||||
'unit_amount': 2.0,
|
||||
})
|
||||
|
||||
sequence_per_invoice_type = self.project_task_rate._get_profitability_sequence_per_invoice_type()
|
||||
self.assertIn('billable_time', sequence_per_invoice_type)
|
||||
self.assertIn('billable_fixed', sequence_per_invoice_type)
|
||||
self.assertIn('billable_milestones', sequence_per_invoice_type)
|
||||
self.assertIn('billable_manual', sequence_per_invoice_type)
|
||||
|
||||
self.assertEqual(self.task.sale_line_id, delivery_service_order_line)
|
||||
self.assertEqual((timesheet1 + timesheet2).so_line, delivery_service_order_line)
|
||||
self.assertEqual(delivery_service_order_line.qty_delivered, 0.0, 'The service type is not timesheet but manual so the quantity delivered is not increased by the timesheets linked.')
|
||||
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [],
|
||||
'total': {'to_invoice': 0.0, 'invoiced': 0.0},
|
||||
'data': [{'id': 'other_revenues_aal', 'sequence': sequence_per_invoice_type['other_revenues_aal'], 'invoiced': 100.0, 'to_invoice': 0.0}],
|
||||
'total': {'invoiced': 100.0, 'to_invoice': 0.0},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_costs_aal', 'sequence': sequence_per_invoice_type['other_costs_aal'], 'billed': -100.0, 'to_bill': 0.0},
|
||||
{
|
||||
'id': 'billable_manual',
|
||||
'sequence': sequence_per_invoice_type['billable_manual'],
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost,
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost
|
||||
'billed': -100 + (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Create a 3rd foreign timesheet and manually update it.
|
||||
foreign_timesheet3 = Timesheet.create({
|
||||
'name': 'Foreign_Timesheet 3',
|
||||
'employee_id': foreign_employee_2.id,
|
||||
'project_id': self.project_task_rate.id,
|
||||
'unit_amount': 1.0,
|
||||
'so_line': False,
|
||||
'is_so_line_edited': True,
|
||||
'company_id': foreign_company.id,
|
||||
})
|
||||
self.assertFalse(foreign_timesheet3.so_line, 'This timesheet should be non billable since the user manually empty the SOL.')
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [{'id': 'other_revenues_aal', 'sequence': sequence_per_invoice_type['other_revenues_aal'], 'invoiced': 100.0, 'to_invoice': 0.0}],
|
||||
'total': {'invoiced': 100.0, 'to_invoice': 0.0},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_costs_aal', 'sequence': sequence_per_invoice_type['other_costs_aal'], 'billed': -100.0, 'to_bill': 0.0},
|
||||
{
|
||||
'id': 'billable_manual',
|
||||
'sequence': sequence_per_invoice_type['billable_manual'],
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
{
|
||||
'id': 'non_billable',
|
||||
'sequence': sequence_per_invoice_type['non_billable'],
|
||||
'billed': foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': -100 + (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2 +
|
||||
foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Create a 3rd timesheet and manually update it.
|
||||
timesheet3 = Timesheet.create({
|
||||
'name': 'Timesheet 3',
|
||||
'employee_id': self.employee_manager.id,
|
||||
|
|
@ -121,106 +260,298 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet):
|
|||
self.project_task_rate._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [],
|
||||
'total': {'to_invoice': 0.0, 'invoiced': 0.0},
|
||||
'data': [{'id': 'other_revenues_aal', 'sequence': sequence_per_invoice_type['other_revenues_aal'], 'invoiced': 100.0, 'to_invoice': 0.0}],
|
||||
'total': {'invoiced': 100.0, 'to_invoice': 0.0},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_costs_aal', 'sequence': sequence_per_invoice_type['other_costs_aal'], 'billed': -100.0, 'to_bill': 0.0},
|
||||
{
|
||||
'id': 'billable_manual',
|
||||
'sequence': sequence_per_invoice_type['billable_manual'],
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost,
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
{
|
||||
'id': 'non_billable',
|
||||
'sequence': sequence_per_invoice_type['non_billable'],
|
||||
'billed': timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'billed': foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2 + timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed':
|
||||
(timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost
|
||||
+ timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'billed': -100 + (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2 +
|
||||
foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2 + timesheet3.unit_amount * -self.employee_manager.hourly_cost
|
||||
},
|
||||
},
|
||||
},
|
||||
'The previous costs should remains and the cost of the third timesheet should be added.'
|
||||
}
|
||||
)
|
||||
|
||||
delivery_timesheet_order_line = SaleOrderLine.create({
|
||||
# Create a new foreign sol, and link this sol to the so_line of the task.
|
||||
foreign_delivery_timesheet_order_line = SaleOrderLineForeign.create({
|
||||
'product_id': self.product_delivery_timesheet1.id,
|
||||
'product_uom_qty': 5,
|
||||
})
|
||||
self.task.write({'sale_line_id': delivery_timesheet_order_line.id})
|
||||
billable_timesheets = timesheet1 + timesheet2
|
||||
self.assertEqual(billable_timesheets.so_line, delivery_timesheet_order_line, 'The SOL of the timesheets should be the one of the task.')
|
||||
self.assertEqual(delivery_timesheet_order_line.qty_delivered, timesheet1.unit_amount + timesheet2.unit_amount)
|
||||
self.assertEqual(
|
||||
self.task.write({'sale_line_id': foreign_delivery_timesheet_order_line.id})
|
||||
billable_timesheets = timesheet1 + timesheet2 + foreign_timesheet1 + foreign_timesheet2
|
||||
self.assertEqual(billable_timesheets.so_line, foreign_delivery_timesheet_order_line, 'The SOL of the timesheets should be the one of the task.')
|
||||
self.assertEqual(foreign_delivery_timesheet_order_line.qty_delivered, timesheet1.unit_amount + timesheet2.unit_amount + foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount,
|
||||
'Since the product type of the SOL is "delivered on TS", the qty_delivered of the SOL should be the total of unit amount of the TS.')
|
||||
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [
|
||||
{'id': 'billable_time', 'sequence': sequence_per_invoice_type['billable_time'], 'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
|
||||
],
|
||||
'total': {'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_revenues_aal', 'sequence': sequence_per_invoice_type['other_revenues_aal'], 'invoiced': 100.0, 'to_invoice': 0.0},
|
||||
{
|
||||
'id': 'billable_time',
|
||||
'sequence': sequence_per_invoice_type['billable_time'],
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost,
|
||||
'to_invoice': foreign_delivery_timesheet_order_line.untaxed_amount_to_invoice * 0.2,
|
||||
'invoiced': 0.0
|
||||
},
|
||||
],
|
||||
'total': {'invoiced': 100.0, 'to_invoice': foreign_delivery_timesheet_order_line.untaxed_amount_to_invoice * 0.2},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_costs_aal', 'sequence': sequence_per_invoice_type['other_costs_aal'], 'billed': -100.0, 'to_bill': 0.0},
|
||||
{
|
||||
'id': 'billable_time',
|
||||
'sequence': sequence_per_invoice_type['billable_time'],
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
{
|
||||
'id': 'non_billable',
|
||||
'sequence': sequence_per_invoice_type['non_billable'],
|
||||
'billed': timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'billed': foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2 + timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed':
|
||||
(timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost
|
||||
+ timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'billed': -100 + (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2 +
|
||||
foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2 + timesheet3.unit_amount * -self.employee_manager.hourly_cost
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
milestone_order_line = SaleOrderLine.create({
|
||||
'product_id': self.product_milestone.id,
|
||||
'product_uom_qty': 1,
|
||||
# Create a new task in the project, link to it a new SO form the main company SO with a delivery timesheet product.
|
||||
delivery_timesheet_order_line = SaleOrderLine.create({
|
||||
'product_id': self.product_delivery_timesheet1.id,
|
||||
'product_uom_qty': 5,
|
||||
})
|
||||
task2 = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Test',
|
||||
task_2 = self.env['project.task'].create({
|
||||
'name': 'Task 2',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'sale_line_id': milestone_order_line.id,
|
||||
'sale_line_id': delivery_timesheet_order_line.id,
|
||||
})
|
||||
task2_timesheet = Timesheet.with_context(default_task_id=task2.id).create({
|
||||
task2_timesheet = Timesheet.with_context(default_task_id=task_2.id).create({
|
||||
'name': '/',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': 1,
|
||||
})
|
||||
self.assertEqual(task2_timesheet.so_line, milestone_order_line)
|
||||
self.assertNotEqual(delivery_timesheet_order_line.untaxed_amount_to_invoice, 0.0)
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [
|
||||
{'id': 'other_revenues_aal', 'sequence': sequence_per_invoice_type['other_revenues_aal'],
|
||||
'invoiced': 100.0, 'to_invoice': 0.0},
|
||||
{
|
||||
'id': 'billable_time',
|
||||
'sequence': sequence_per_invoice_type['billable_time'],
|
||||
'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice + foreign_delivery_timesheet_order_line.untaxed_amount_to_invoice * 0.2,
|
||||
'invoiced': 0.0
|
||||
},
|
||||
],
|
||||
'total': {'invoiced': 100.0, 'to_invoice': foreign_delivery_timesheet_order_line.untaxed_amount_to_invoice * 0.2 + delivery_timesheet_order_line.untaxed_amount_to_invoice},
|
||||
},
|
||||
'costs': {
|
||||
'data': [
|
||||
{'id': 'other_costs_aal', 'sequence': sequence_per_invoice_type['other_costs_aal'], 'billed': -100.0,
|
||||
'to_bill': 0.0},
|
||||
{
|
||||
'id': 'billable_time',
|
||||
'sequence': sequence_per_invoice_type['billable_time'],
|
||||
'billed': (timesheet1.unit_amount + timesheet2.unit_amount + task2_timesheet.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
{
|
||||
'id': 'non_billable',
|
||||
'sequence': sequence_per_invoice_type['non_billable'],
|
||||
'billed': foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2 + timesheet3.unit_amount * -self.employee_manager.hourly_cost,
|
||||
'to_bill': 0.0,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': -100 + (timesheet1.unit_amount + timesheet2.unit_amount + task2_timesheet.unit_amount) * -self.employee_user.hourly_cost +
|
||||
(foreign_timesheet1.unit_amount + foreign_timesheet2.unit_amount) * -foreign_employee.hourly_cost * 0.2 +
|
||||
foreign_timesheet3.unit_amount * -foreign_employee_2.hourly_cost * 0.2 + timesheet3.unit_amount * -self.employee_manager.hourly_cost
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
# Create a SOL in the foreign SO with a milestone service product.
|
||||
milestone_foreign_order_line = SaleOrderLineForeign.create({
|
||||
'product_id': self.product_milestone.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
task2_foreign = self.env['project.task'].create({
|
||||
'name': 'Test',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'sale_line_id': milestone_foreign_order_line.id,
|
||||
})
|
||||
task2_foreign_timesheet = Timesheet.with_context(default_task_id=task2_foreign.id).create({
|
||||
'name': '/',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'employee_id': foreign_employee.id,
|
||||
'unit_amount': 1,
|
||||
})
|
||||
self.assertEqual(task2_foreign_timesheet.so_line, milestone_foreign_order_line)
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertFalse([data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'])
|
||||
self.assertDictEqual(
|
||||
[data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'][0],
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_bill': 0.0, 'billed': task2_timesheet.amount},
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_bill': 0.0, 'billed': task2_foreign_timesheet.amount * 0.2},
|
||||
)
|
||||
milestone_foreign_order_line.qty_delivered = 1
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
[data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'][0],
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'],
|
||||
'to_invoice': milestone_foreign_order_line.untaxed_amount_to_invoice * 0.2, 'invoiced': 0.0},
|
||||
)
|
||||
# Create a second timesheet in the new task, with an employee from the main company.
|
||||
task2_timesheet = Timesheet.with_context(default_task_id=task2_foreign.id).create({
|
||||
'name': '/',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': 1,
|
||||
})
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
[data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'][0],
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_bill': 0.0,
|
||||
'billed': task2_timesheet.amount + task2_foreign_timesheet.amount * 0.2},
|
||||
)
|
||||
milestone_foreign_order_line.qty_delivered = 2
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
[data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'][0],
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'],
|
||||
'to_invoice': milestone_foreign_order_line.untaxed_amount_to_invoice * 0.2, 'invoiced': 0.0},
|
||||
)
|
||||
# Create a SOL in the foreign SO with a milestone service product.
|
||||
milestone_order_line = SaleOrderLine.create({
|
||||
'product_id': self.product_milestone.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
task3_milestone = self.env['project.task'].create({
|
||||
'name': 'Task 3',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'sale_line_id': milestone_order_line.id,
|
||||
})
|
||||
task3_timesheet = Timesheet.with_context(default_task_id=task3_milestone.id).create({
|
||||
'name': '/',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': 1,
|
||||
})
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
[data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'][0],
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_bill': 0.0,
|
||||
'billed': task2_timesheet.amount + task2_foreign_timesheet.amount * 0.2 + task3_timesheet.amount},
|
||||
)
|
||||
|
||||
milestone_order_line.qty_delivered = 1
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertDictEqual(
|
||||
[data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'][0],
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_invoice': milestone_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
|
||||
{'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'],
|
||||
'to_invoice': milestone_foreign_order_line.untaxed_amount_to_invoice * 0.2 + milestone_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0},
|
||||
)
|
||||
|
||||
# Cancel the milestone timesheets
|
||||
task2_timesheet.unlink()
|
||||
task2_foreign_timesheet.unlink()
|
||||
task3_timesheet.unlink()
|
||||
profitability_items = self.project_task_rate._get_profitability_items(False)
|
||||
self.assertFalse([data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'])
|
||||
self.assertFalse([data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'])
|
||||
|
||||
def test_profitability_revenue(self):
|
||||
product_profitability_items = self.env['product.product'].create({
|
||||
'name': "Service Ordered, create task in new project",
|
||||
'standard_price': 10,
|
||||
'list_price': 20,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': self.uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED3',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': self.project_task_rate.id,
|
||||
'service_type': 'manual',
|
||||
})
|
||||
saleorder_revenue = self.env['sale.order'].with_context(tracking_disable=True)
|
||||
saleOrderLine_revenue = self.env['sale.order.line'].with_context(tracking_disable=True)
|
||||
sale_order_revenue = saleorder_revenue.create({
|
||||
'partner_id': self.partner_b.id,
|
||||
'partner_invoice_id': self.partner_b.id,
|
||||
'partner_shipping_id': self.partner_b.id,
|
||||
})
|
||||
|
||||
sale_order_line_revenue = saleOrderLine_revenue.create({
|
||||
'product_id': product_profitability_items.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order_revenue.id,
|
||||
})
|
||||
|
||||
sequence_per_invoice_type = self.project_task_rate._get_profitability_sequence_per_invoice_type()
|
||||
profitability_item_data = {
|
||||
'revenues': {
|
||||
'data': [{
|
||||
'id': 'billable_fixed',
|
||||
'sequence': sequence_per_invoice_type['billable_fixed'],
|
||||
'invoiced': 0.0,
|
||||
'to_invoice': 200.0
|
||||
}],
|
||||
'total': {
|
||||
'invoiced': 0.0,
|
||||
'to_invoice': 200.0
|
||||
}
|
||||
},
|
||||
'costs': {
|
||||
'data': [],
|
||||
'total': {'billed': 0.0, 'to_bill': 0.0}
|
||||
}
|
||||
}
|
||||
|
||||
sale_order_revenue.action_confirm()
|
||||
project_profitability_items = sale_order_line_revenue.project_id
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
profitability_item_data
|
||||
)
|
||||
project_profitability_items.active = False
|
||||
self.assertDictEqual(
|
||||
self.project_task_rate._get_profitability_items(False),
|
||||
profitability_item_data
|
||||
)
|
||||
profitability_before = self.project_task_rate.get_panel_data()['profitability_items']
|
||||
product_profitability_items.active = False
|
||||
profitability_after = self.project_task_rate.get_panel_data()['profitability_items']
|
||||
self.assertEqual(
|
||||
profitability_before,
|
||||
profitability_after,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,10 @@ class TestProjectUpdateSaleTimesheet(TestProjectUpdate):
|
|||
# Store the formatted amount in the same currency as the project to guarantee the same unit
|
||||
comparison_amount = format_amount(self.env, 0.0, self.project_pigs.currency_id)
|
||||
|
||||
self.assertEqual(template_values['profitability']['costs'], 0.0, "Project costs used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['costs_formatted'], comparison_amount, "Project costs used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['revenues'], 0.0, "Project revenues used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['revenues_formatted'], comparison_amount, "Project revenues used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['margin'], 0, "Margin used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['margin_formatted'], comparison_amount, "Margin formatted used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['margin_percentage'], "0", "Margin percentage used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['total']['costs'], 0.0, "Project costs used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['total']['revenues'], 0.0, "Project revenues used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['total']['margin'], 0, "Margin used in the template should be well defined")
|
||||
self.assertEqual(template_values['profitability']['total']['margin_percentage'], "0", "Margin percentage used in the template should be well defined")
|
||||
|
||||
def test_project_update_panel_profitability_no_billable(self):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ from odoo.tests import Form, tagged
|
|||
class TestReInvoice(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# patch expense products to make them services creating task/project
|
||||
service_values = {
|
||||
|
|
@ -31,8 +31,7 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
|
||||
# create AA, SO and invoices
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'name': 'Timesheet Plan',
|
||||
})
|
||||
|
||||
cls.analytic_account = cls.env['account.analytic.account'].create({
|
||||
|
|
@ -43,34 +42,39 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
cls.project = cls.env['project.project'].create({
|
||||
'name': 'SO Project',
|
||||
'allow_timesheets': False,
|
||||
f'{cls.analytic_plan._column_name()}': cls.analytic_account.id,
|
||||
})
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account.id,
|
||||
'pricelist_id': cls.company_data['default_pricelist'].id,
|
||||
'project_id': cls.project.id,
|
||||
})
|
||||
|
||||
cls.Invoice = cls.env['account.move'].with_context(
|
||||
default_move_type='in_invoice',
|
||||
default_invoice_date=cls.sale_order.date_order,
|
||||
mail_notrack=True,
|
||||
mail_create_nolog=True,
|
||||
)
|
||||
|
||||
def test_at_cost(self):
|
||||
""" Test vendor bill at cost for product based on ordered and delivered quantities. """
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
# Required for `analytic_distribution` to be visible in the view
|
||||
self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')
|
||||
# create SO line and confirm SO (with only one line)
|
||||
sale_order_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_cost'].id,
|
||||
'product_uom_qty': 2,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_hour').id,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
sale_order_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_cost'].id,
|
||||
'product_uom_qty': 4,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_hour').id,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
||||
|
|
@ -116,10 +120,10 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
|
||||
self.assertFalse(sale_order_line3.task_id, "Adding a new expense SO line should not create a task (sol3)")
|
||||
self.assertFalse(sale_order_line4.task_id, "Adding a new expense SO line should not create a task (sol4)")
|
||||
self.assertEqual(len(self.sale_order.project_ids), 1, "SO create only one project with its service line. Adding new expense SO line should not impact that")
|
||||
self.assertEqual(len(self.sale_order.order_line.mapped('project_id')), 1, "SO create only one project with its service line. Adding new expense SO line should not impact that")
|
||||
|
||||
self.assertEqual((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.company_data['product_order_cost'].standard_price, 3.0, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.company_data['product_delivery_cost'].standard_price, 3.0, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.company_data['product_order_cost'].standard_price, 3.0, 3.0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.company_data['product_delivery_cost'].standard_price, 3.0, 3.0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
|
||||
self.assertEqual(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount")
|
||||
self.assertEqual(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line should be computed by analytic amount")
|
||||
|
|
@ -147,25 +151,27 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
self.assertEqual(len(self.sale_order.order_line), 6, "There should be still 4 lines on the SO, no new created")
|
||||
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 4, "There should be still 2 expenses lines on the SO")
|
||||
|
||||
self.assertEqual((sale_order_line5.price_unit, sale_order_line5.qty_delivered, sale_order_line5.product_uom_qty, sale_order_line5.qty_invoiced), (self.company_data['product_order_cost'].standard_price, 2.0, 0, 0), 'Sale line 5 is wrong after confirming 2e vendor invoice')
|
||||
self.assertEqual((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line6.product_uom_qty, sale_order_line6.qty_invoiced), (self.company_data['product_delivery_cost'].standard_price, 2.0, 0, 0), 'Sale line 6 is wrong after confirming 2e vendor invoice')
|
||||
self.assertEqual((sale_order_line5.price_unit, sale_order_line5.qty_delivered, sale_order_line5.product_uom_qty, sale_order_line5.qty_invoiced), (self.company_data['product_order_cost'].standard_price, 2.0, 2.0, 0), 'Sale line 5 is wrong after confirming 2e vendor invoice')
|
||||
self.assertEqual((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line6.product_uom_qty, sale_order_line6.qty_invoiced), (self.company_data['product_delivery_cost'].standard_price, 2.0, 2.0, 0), 'Sale line 6 is wrong after confirming 2e vendor invoice')
|
||||
|
||||
def test_sales_price(self):
|
||||
""" Test invoicing vendor bill at sales price for products based on delivered and ordered quantities. Check no existing SO line is incremented, but when invoicing a
|
||||
second time, increment only the delivered so line.
|
||||
"""
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
# Required for `analytic_distribution` to be visible in the view
|
||||
self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')
|
||||
# create SO line and confirm SO (with only one line)
|
||||
sale_order_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_delivery_sales_price'].id,
|
||||
'product_uom_qty': 2,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_hour').id,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
sale_order_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_sales_price'].id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_hour').id,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
|
@ -208,10 +214,10 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
|
||||
self.assertFalse(sale_order_line3.task_id, "Adding a new expense SO line should not create a task (sol3)")
|
||||
self.assertFalse(sale_order_line4.task_id, "Adding a new expense SO line should not create a task (sol4)")
|
||||
self.assertEqual(len(self.sale_order.project_ids), 1, "SO create only one project with its service line. Adding new expense SO line should not impact that")
|
||||
self.assertEqual(len(self.sale_order.order_line.mapped('project_id')), 1, "SO create only one project with its service line. Adding new expense SO line should not impact that")
|
||||
|
||||
self.assertEqual((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.company_data['product_delivery_sales_price'].list_price, 3.0, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.company_data['product_order_sales_price'].list_price, 3.0, 0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line3.price_unit, sale_order_line3.qty_delivered, sale_order_line3.product_uom_qty, sale_order_line3.qty_invoiced), (self.company_data['product_delivery_sales_price'].list_price, 3.0, 3.0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
self.assertEqual((sale_order_line4.price_unit, sale_order_line4.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line4.qty_invoiced), (self.company_data['product_order_sales_price'].list_price, 3.0, 3.0, 0), 'Sale line is wrong after confirming vendor invoice')
|
||||
|
||||
self.assertEqual(sale_order_line3.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 3 should be computed by analytic amount")
|
||||
self.assertEqual(sale_order_line4.qty_delivered_method, 'analytic', "Delivered quantity of 'expense' SO line 4 should be computed by analytic amount")
|
||||
|
|
@ -239,16 +245,17 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
self.assertEqual(len(self.sale_order.order_line), 5, "There should be 5 lines on the SO, 1 new created and 1 incremented")
|
||||
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 3, "There should be 3 expenses lines on the SO")
|
||||
|
||||
self.assertEqual((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line6.qty_invoiced), (self.company_data['product_order_sales_price'].list_price, 2.0, 0, 0), 'Sale line is wrong after confirming 2e vendor invoice')
|
||||
self.assertEqual((sale_order_line6.price_unit, sale_order_line6.qty_delivered, sale_order_line4.product_uom_qty, sale_order_line6.qty_invoiced), (self.company_data['product_order_sales_price'].list_price, 2.0, 3.0, 0), 'Sale line is wrong after confirming 2e vendor invoice')
|
||||
|
||||
def test_no_expense(self):
|
||||
""" Test invoicing vendor bill with no policy. Check nothing happen. """
|
||||
# Required for `analytic_account_id` to be visible in the view
|
||||
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
||||
# Required for `analytic_distribution` to be visible in the view
|
||||
self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')
|
||||
# confirm SO
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': 2,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_hour').id,
|
||||
'qty_delivered': 1,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
|
@ -334,7 +341,6 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
"active_model": 'sale.order',
|
||||
"active_ids": [sale_order.id],
|
||||
"active_id": sale_order.id,
|
||||
'open_invoices': True,
|
||||
}
|
||||
# Invoice the 1
|
||||
wizard = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
|
|
@ -353,10 +359,9 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
}
|
||||
refund_invoice_wiz = self.env['account.move.reversal'].with_context(wiz_context).create({
|
||||
'reason': 'please reverse :c',
|
||||
'refund_method': 'refund',
|
||||
'date': today,
|
||||
})
|
||||
refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
|
||||
refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.refund_moves()['res_id'])
|
||||
refund_invoice.action_post()
|
||||
# reversing with action_reverse and then action_post does not reset the invoice_status to 'to invoice' in tests
|
||||
|
||||
|
|
@ -369,3 +374,47 @@ class TestReInvoice(TestCommonSaleTimesheet):
|
|||
|
||||
# The actual test :
|
||||
wizard.create_invoices() # No exception should be raised, there is indeed something to be invoiced since it was reversed
|
||||
|
||||
def test_project_update_reinvoiced_vendor_bill_product(self):
|
||||
project_product, expense_product = self.env['product.product'].create([{
|
||||
'name': 'Project creation',
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_in_project',
|
||||
}, {
|
||||
'name': 'Expense Product',
|
||||
'expense_policy': 'sales_price',
|
||||
'list_price': 20,
|
||||
}])
|
||||
sale_order = self.env['sale.order'].create({'partner_id': self.partner_a.id})
|
||||
self.env['sale.order.line'].create({
|
||||
'product_id': project_product.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
project = sale_order.project_ids
|
||||
self.assertTrue(project, 'Project should have been created on sale order confirmation')
|
||||
|
||||
vendor_bill = self.env['account.move'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'invoice_date': sale_order.date_order,
|
||||
'journal_id': self.env['account.journal'].search([('code', '=', 'BILL'), ('company_id', '=', self.env.company.id)]).id,
|
||||
'move_type': 'in_invoice',
|
||||
})
|
||||
self.env['account.move.line'].create({
|
||||
'product_id': expense_product.id,
|
||||
'move_id': vendor_bill.id,
|
||||
'account_id': self.env['account.account'].search([('code', '=', '600000')]).id,
|
||||
'analytic_distribution': {project.account_id.id: 100},
|
||||
'price_unit': 20,
|
||||
})
|
||||
vendor_bill.action_post() # An analytic line is created for the vendor bill move line
|
||||
self.assertEqual(project.account_id.vendor_bill_count, 1, 'Vendor bill should be linked to project account')
|
||||
self.assertTrue(vendor_bill.line_ids.analytic_line_ids, 'Analytic line should be created for the account move line')
|
||||
self.assertTrue(sale_order.order_line.analytic_line_ids, 'Analytic line should be linked to the sale order line created by the re-invoiced expense product')
|
||||
|
||||
# Only the original vendor bill amount should appear on the project update, to stay consistent with the corresponding hr_expense behavior
|
||||
updates = project._get_profitability_items()
|
||||
data_line = updates['costs']['data'][0]
|
||||
self.assertEqual(data_line['id'], 'other_purchase_costs')
|
||||
self.assertEqual(data_line['billed'], -20)
|
||||
self.assertEqual(updates['costs']['total']['billed'], -20, 'Only the vendor bill should be deducted')
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ 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)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
cls.sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
|
|
@ -38,7 +38,6 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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')
|
||||
self.assertEqual(task.email_from, self.sale_order.partner_id.email, 'Sale Service: Task Email should be the same as the SO customer Email')
|
||||
|
||||
# log timesheet on task
|
||||
self.env['account.analytic.line'].create({
|
||||
|
|
@ -63,7 +62,6 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'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',
|
||||
|
|
@ -93,7 +91,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'order_id': self.sale_order.id,
|
||||
'product_id': self.product_delivery_timesheet3.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': uom_days.id,
|
||||
'product_uom_id': uom_days.id,
|
||||
})
|
||||
self.sale_order.action_confirm()
|
||||
task = self.env['project.task'].search([('sale_line_id', '=', sale_order_line.id)])
|
||||
|
|
@ -223,10 +221,11 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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.planned_hours, "The planned hours should be the same as the ordered quantity of the native SO line")
|
||||
self.assertEqual(so_line1.product_uom_qty, so_line1.project_id.allocated_hours, "The planned hours on the project should be the same as the ordered quantity of the native SO line")
|
||||
self.assertEqual(so_line1.product_uom_qty, so_line1.task_id.allocated_hours, "The planned hours on the task 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.planned_hours, "The planned hours should have changed when updating the ordered quantity of the native SO line")
|
||||
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()
|
||||
|
|
@ -237,7 +236,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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.planned_hours, "The planned hours should have changed when updating the ordered quantity, even after SO cancellation")
|
||||
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()
|
||||
|
|
@ -248,7 +247,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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_done()
|
||||
self.sale_order.action_lock()
|
||||
with self.assertRaises(UserError):
|
||||
so_line1.write({'product_uom_qty': 20})
|
||||
|
||||
|
|
@ -282,7 +281,6 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'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',
|
||||
|
|
@ -352,60 +350,6 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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).
|
||||
|
|
@ -478,7 +422,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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({'display_project_id': self.project_global.id})
|
||||
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)
|
||||
|
|
@ -503,16 +447,16 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
})
|
||||
|
||||
self.sale_order.action_confirm()
|
||||
self.assertEqual(sale_order_line.product_uom_qty, sale_order_line.task_id.planned_hours, "The planned hours should be the same as the ordered quantity of the native SO line")
|
||||
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.planned_hours, "The planned hours should have changed when updating the ordered quantity of the native SO line")
|
||||
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.planned_hours, "The planned hours should have changed when updating the ordered quantity, even after SO cancellation")
|
||||
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_done()
|
||||
self.sale_order.action_lock()
|
||||
with self.assertRaises(UserError):
|
||||
sale_order_line.write({'product_uom_qty': 20})
|
||||
|
||||
|
|
@ -596,11 +540,10 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
self.assertEqual(prepaid_service_sol.remaining_hours, 2, "The remaining hours should not change.")
|
||||
|
||||
def test_several_uom_sol_to_planned_hours(self):
|
||||
planned_hours_for_uom = {
|
||||
allocated_hours_for_uom = {
|
||||
'day': 8.0,
|
||||
'hour': 1.0,
|
||||
'unit': 1.0,
|
||||
'gram': 0.0,
|
||||
}
|
||||
|
||||
project = self.project_global.copy({'tasks': False})
|
||||
|
|
@ -619,20 +562,18 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'order_id': self.sale_order.id,
|
||||
}
|
||||
|
||||
for uom_name in planned_hours_for_uom:
|
||||
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,
|
||||
'product_uom_id': uom_id.id,
|
||||
})
|
||||
SaleOrderLine.create(sol_vals)
|
||||
|
||||
|
|
@ -640,14 +581,12 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
|
||||
tasks = project.task_ids
|
||||
for task in tasks:
|
||||
self.assertEqual(task.planned_hours, planned_hours_for_uom[task.sale_line_id.name])
|
||||
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']
|
||||
|
||||
|
|
@ -664,7 +603,6 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'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()
|
||||
|
|
@ -676,7 +614,6 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'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,
|
||||
})
|
||||
|
|
@ -684,7 +621,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
|
||||
# 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},
|
||||
self.assertEqual(invoice_line.analytic_distribution, {str(self.project_global.account_id.id): 100},
|
||||
"SOL's analytic distribution should contain the project analytic account")
|
||||
|
||||
def test_sale_timesheet_invoice(self):
|
||||
|
|
@ -750,12 +687,10 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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
|
||||
1) Create a 2 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
|
||||
|
|
@ -765,26 +700,16 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
'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
|
||||
'product_uom_id': 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
|
||||
'product_uom_id': self.env.ref('uom.product_uom_hour').id, # 8 hours
|
||||
}])
|
||||
self.sale_order.action_confirm()
|
||||
allocated_hours = self.sale_order.project_ids.allocated_hours
|
||||
self.assertEqual(16 + 8 + 6, allocated_hours,
|
||||
self.assertEqual(16 + 8, allocated_hours,
|
||||
"Project's allocated hours should add up correctly.")
|
||||
|
||||
self.env.company.timesheet_encode_uom_id = self.env.ref('uom.product_uom_day')
|
||||
|
|
@ -793,16 +718,103 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
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'].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")
|
||||
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")
|
||||
|
||||
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'].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")
|
||||
|
||||
def test_timesheet_hours_delivered_rounding(self):
|
||||
"""
|
||||
Ensure hours are rounded consistently on SO & invoice.
|
||||
"""
|
||||
self.env.company.project_time_mode_id.rounding = 1.0
|
||||
self.env['decimal.precision'].search([('name', '=', 'Product Unit')]).digits = 0
|
||||
self.product_delivery_timesheet3.uom_id._invalidate_cache(['rounding'])
|
||||
self.env['sale.order.line'].create({
|
||||
'name': self.product_delivery_timesheet3.name,
|
||||
'product_id': self.product_delivery_timesheet3.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.product_delivery_timesheet3.uom_id.id,
|
||||
'price_unit': self.product_delivery_timesheet3.list_price,
|
||||
'order_id': self.sale_order.id,
|
||||
})
|
||||
|
|
@ -833,3 +845,38 @@ class TestSaleService(TestCommonSaleTimesheet):
|
|||
hours_delivered,
|
||||
f"{amount} hours delivered should round the same for invoice & timesheet",
|
||||
)
|
||||
|
||||
def test_prepaid_pack_remaining_hours_rounding(self):
|
||||
"""Avoid double rounding with pack UoM"""
|
||||
uom_hour = self.env.ref('uom.product_uom_hour')
|
||||
pack20 = self.env['uom.uom'].create({
|
||||
'name': 'Pack of 20 Hours',
|
||||
'relative_factor': 20.0,
|
||||
'relative_uom_id': uom_hour.id,
|
||||
})
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Prepaid Pack 20h',
|
||||
'type': 'service',
|
||||
'uom_id': pack20.id,
|
||||
'service_type': 'timesheet',
|
||||
'service_policy': 'ordered_prepaid',
|
||||
'service_tracking': 'task_in_project',
|
||||
})
|
||||
order = self.env['sale.order'].create({'partner_id': self.partner_a.id})
|
||||
sol = self.env['sale.order.line'].create({
|
||||
'order_id': order.id,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'product_uom_id': pack20.id,
|
||||
})
|
||||
order.action_confirm()
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': 'Over-consumed timesheet',
|
||||
'project_id': sol.project_id.id,
|
||||
'task_id': sol.task_id.id,
|
||||
'unit_amount': 22.0,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
sol.invalidate_recordset()
|
||||
self.assertAlmostEqual(sol.remaining_hours, -2.0, places=6)
|
||||
self.assertIn('-02:00', sol.with_context(with_remaining_hours=True).display_name)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import Command
|
||||
from odoo.fields import Date
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.addons.hr_timesheet.tests.test_timesheet import TestCommonTimesheet
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.tests import Form, tagged, new_test_user
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleTimesheet(TestCommonSaleTimesheet):
|
||||
|
|
@ -20,6 +19,45 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
quantities changes, ...
|
||||
"""
|
||||
|
||||
def test_compute_commercial_partner(self):
|
||||
"""Ensure user without project access can compute commercial partner without AccessError.
|
||||
Steps:
|
||||
1. Create a commercial partner and a sub-partner.
|
||||
2. Create a project assigned to the sub-partner and a task under that project. Link both to a timesheet.
|
||||
3. Create a restricted user with no access to the Project module but with Timesheet Administrator access.
|
||||
4. Compute the commercial partner as the restricted user and verify it's derived from the project partner.
|
||||
5. Set the task partner, recompute, and verify the commercial partner updates accordingly.
|
||||
"""
|
||||
commercial_partner = self.env['res.partner'].create({'name': 'Commercial Partner', 'is_company': True})
|
||||
sub_partner = self.env['res.partner'].create({'name': 'Sub Partner', 'parent_id': commercial_partner.id})
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test Project',
|
||||
'partner_id': sub_partner.id,
|
||||
'privacy_visibility': 'followers',
|
||||
'task_ids': [Command.create({'name': 'Test Task'})]
|
||||
})
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Timesheet',
|
||||
'project_id': project.id,
|
||||
'task_id': project.task_ids[0].id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
timesheet_manager_no_project_user = new_test_user(self.env, login='no_project_user', groups='hr_timesheet.group_timesheet_manager')
|
||||
|
||||
timesheet.with_user(timesheet_manager_no_project_user)._compute_commercial_partner()
|
||||
self.assertEqual(
|
||||
timesheet.commercial_partner_id,
|
||||
commercial_partner,
|
||||
"The commercial partner should match the partner linked to the project."
|
||||
)
|
||||
project.task_ids[0].partner_id = sub_partner.id
|
||||
timesheet.with_user(timesheet_manager_no_project_user)._compute_commercial_partner()
|
||||
self.assertEqual(
|
||||
timesheet.commercial_partner_id,
|
||||
commercial_partner,
|
||||
"The commercial partner should match the partner linked to the task."
|
||||
)
|
||||
|
||||
def test_timesheet_order(self):
|
||||
""" Test timesheet invoicing with 'invoice on order' timetracked products
|
||||
1. create SO with 2 ordered product and confirm
|
||||
|
|
@ -33,7 +71,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
so_line_ordered_project_only = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet4.id,
|
||||
|
|
@ -51,7 +88,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
|
||||
self.assertEqual(sale_order.tasks_count, 1, "One task should have been created on SO confirmation")
|
||||
self.assertEqual(len(sale_order.project_ids), 2, "One project should have been created by the SO, when confirmed + the one from SO line 2 'task in global project'")
|
||||
self.assertEqual(sale_order.analytic_account_id, project_serv1.analytic_account_id, "The created project should be linked to the analytic account of the SO")
|
||||
self.assertEqual(sale_order.project_account_id, project_serv1.account_id, "The created project should be linked to the analytic account of the SO")
|
||||
|
||||
# create invoice
|
||||
invoice1 = sale_order._create_invoices()[0]
|
||||
|
|
@ -160,7 +197,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
so_line_deliver_global_project = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
|
|
@ -183,10 +219,10 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
self.assertTrue(task_serv1, "Sale Timesheet: on SO confirmation, a task should have been created in global project")
|
||||
self.assertTrue(task_serv2, "Sale Timesheet: on SO confirmation, a task should have been created in a new project")
|
||||
self.assertEqual(sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation')
|
||||
self.assertEqual(sale_order.analytic_account_id, task_serv2.project_id.analytic_account_id, "SO should have create a project")
|
||||
self.assertEqual(sale_order.project_account_id, task_serv2.project_id.account_id, "SO should have create a project")
|
||||
self.assertEqual(sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation")
|
||||
self.assertEqual(len(sale_order.project_ids), 2, "One project should have been created by the SO, when confirmed + the one from SO line 1 'task in global project'")
|
||||
self.assertEqual(sale_order.analytic_account_id, project_serv2.analytic_account_id, "The created project should be linked to the analytic account of the SO")
|
||||
self.assertEqual(sale_order.project_account_id, project_serv2.account_id, "The created project should be linked to the analytic account of the SO")
|
||||
|
||||
# let's log some timesheets
|
||||
timesheet1 = self.env['account.analytic.line'].create({
|
||||
|
|
@ -283,7 +319,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
so_line_manual_global_project = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_manual2.id,
|
||||
|
|
@ -303,7 +338,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
|
||||
project_serv2 = so_line_manual_only_project.project_id
|
||||
self.assertTrue(project_serv2, "A second project is created when selling 'project only' after SO confirmation.")
|
||||
self.assertEqual(sale_order.analytic_account_id, project_serv2.analytic_account_id, "The created project should be linked to the analytic account of the SO")
|
||||
self.assertEqual(sale_order.project_account_id, project_serv2.account_id, "The created project should be linked to the analytic account of the SO")
|
||||
|
||||
# let's log some timesheets (on task and project)
|
||||
timesheet1 = self.env['account.analytic.line'].create({
|
||||
|
|
@ -364,7 +399,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
# Section Line
|
||||
so_line_ordered_project_only = self.env['sale.order.line'].create({
|
||||
|
|
@ -382,6 +416,11 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'product_uom_qty': 20,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
so_line_deliver_timesheet = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_timesheet1.id,
|
||||
'product_uom_qty': 5,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
# confirm SO
|
||||
sale_order.action_confirm()
|
||||
|
|
@ -423,6 +462,22 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'unit_amount': 30,
|
||||
'employee_id': self.employee_manager.id
|
||||
})
|
||||
|
||||
with self.assertRaises(AccessError, msg="The user should not have access to the SOL"):
|
||||
so_line_deliver_timesheet.with_user(self.user_employee_without_sales_access).read(['name'])
|
||||
|
||||
# invalidate cache to make sure the SOL set on the timesheet is not in the cache since the user
|
||||
# should not be able to access on the SOL.
|
||||
self.env['sale.order.line'].invalidate_model()
|
||||
timesheet5 = self.env['account.analytic.line'].with_user(self.user_employee_without_sales_access).create({
|
||||
'name': 'Test Line 5',
|
||||
'project_id': task_serv2.project_id.id,
|
||||
'task_id': task_serv2.id,
|
||||
'unit_amount': 10,
|
||||
'employee_id': self.employee_without_sales_access.id,
|
||||
'so_line': so_line_deliver_timesheet.id,
|
||||
})
|
||||
|
||||
self.assertEqual(so_line_deliver_global_project.invoice_status, 'to invoice')
|
||||
self.assertEqual(so_line_deliver_task_project.invoice_status, 'to invoice')
|
||||
self.assertEqual(sale_order.invoice_status, 'to invoice')
|
||||
|
|
@ -501,7 +556,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
task = Task.with_context(default_project_id=self.project_template.id).create({
|
||||
'name': 'first task',
|
||||
'partner_id': self.partner_b.id,
|
||||
'planned_hours': 10,
|
||||
'allocated_hours': 10,
|
||||
'sale_line_id': self.so.order_line[0].id
|
||||
})
|
||||
|
||||
|
|
@ -518,7 +573,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
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
|
||||
# change project of task, non-validated timesheets will follow the project of task
|
||||
task.write({
|
||||
'project_id': self.project_global.id
|
||||
})
|
||||
|
|
@ -552,14 +607,14 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
|
||||
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.
|
||||
# change project of task, only the timesheet not billed gets its project changed
|
||||
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_count1, 1, "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")
|
||||
|
||||
|
|
@ -568,13 +623,11 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
sale_order2 = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_b.id,
|
||||
'partner_invoice_id': self.partner_b.id,
|
||||
'partner_shipping_id': self.partner_b.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
so1_product_global_project_so_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
|
|
@ -632,7 +685,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
def test_timesheet_upsell(self):
|
||||
""" Test timesheet upselling and email """
|
||||
|
||||
sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
|
|
@ -643,7 +696,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom': uom_days.id,
|
||||
'product_uom_id': uom_days.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
task = sale_order_line.task_id
|
||||
|
|
@ -670,7 +723,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
self.assertEqual(sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
|
||||
message_sent = self.env['mail.message'].search([
|
||||
('id', '>', last_message_id),
|
||||
('subject', 'like', 'Upsell'),
|
||||
('subject', 'like', 'To-Do'),
|
||||
('model', '=', 'sale.order'),
|
||||
('res_id', '=', sale_order.id),
|
||||
])
|
||||
|
|
@ -687,7 +740,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
|
||||
message_sent = self.env['mail.message'].search([
|
||||
('id', '>', last_message_id),
|
||||
('subject', 'like', 'Upsell'),
|
||||
('subject', 'like', 'To-Do'),
|
||||
('model', '=', 'sale.order'),
|
||||
('res_id', '=', sale_order.id),
|
||||
])
|
||||
|
|
@ -696,7 +749,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
def test_timesheet_upsell_copied_so(self):
|
||||
""" Test that copying a SO which had an upsell activity still create an upsell activity on the copy. """
|
||||
|
||||
sale_order = self.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
|
|
@ -707,7 +760,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom': uom_days.id,
|
||||
'product_uom_id': uom_days.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
task = sale_order_line.task_id
|
||||
|
|
@ -734,7 +787,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
self.assertEqual(sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
|
||||
message_sent = self.env['mail.message'].search([
|
||||
('id', '>', last_message_id),
|
||||
('subject', 'like', 'Upsell'),
|
||||
('subject', 'like', 'To-Do'),
|
||||
('model', '=', 'sale.order'),
|
||||
('res_id', '=', sale_order.id),
|
||||
])
|
||||
|
|
@ -751,7 +804,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
|
||||
message_sent = self.env['mail.message'].search([
|
||||
('id', '>', last_message_id),
|
||||
('subject', 'like', 'Upsell'),
|
||||
('subject', 'like', 'To-Do'),
|
||||
('model', '=', 'sale.order'),
|
||||
('res_id', '=', sale_order.id),
|
||||
])
|
||||
|
|
@ -783,7 +836,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
self.assertEqual(sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
|
||||
message_sent = self.env['mail.message'].search([
|
||||
('id', '>', last_message_id),
|
||||
('subject', 'like', 'Upsell'),
|
||||
('subject', 'like', 'To-Do'),
|
||||
('model', '=', 'sale.order'),
|
||||
('res_id', '=', sale_order.id),
|
||||
])
|
||||
|
|
@ -800,7 +853,7 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
|
||||
message_sent = self.env['mail.message'].search([
|
||||
('id', '>', last_message_id),
|
||||
('subject', 'like', 'Upsell'),
|
||||
('subject', 'like', 'To-Do'),
|
||||
('model', '=', 'sale.order'),
|
||||
('res_id', '=', sale_order.id),
|
||||
])
|
||||
|
|
@ -811,13 +864,11 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
})
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'name': self.product_delivery_timesheet2.name,
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 50,
|
||||
'product_uom': self.product_delivery_timesheet2.uom_id.id,
|
||||
'price_unit': self.product_delivery_timesheet2.list_price,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
|
@ -839,9 +890,37 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
move.with_context(check_move_validity=False).line_ids[0].unlink()
|
||||
self.assertFalse(analytic_line.timesheet_invoice_id, "The timesheet should have been unlinked from move")
|
||||
|
||||
def test_update_sol_price(self):
|
||||
""" This test ensure that when the price of a sol is updated, the project_profitability panel from the project linked to the SO of that sol is correctly updated too.
|
||||
1) create new SO
|
||||
2) add a sol with a service product with 'invoice on prepaid' and 'create project & task' setting.
|
||||
3) confirm SO and check the project_profitability panel
|
||||
4) update the price of the sol and check the project_profitability panel
|
||||
"""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
product_price = self.product_order_timesheet3.list_price
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'name': self.product_order_timesheet3.name,
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': product_price,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
project = sale_order.project_ids[0]
|
||||
|
||||
items = project._get_profitability_items(with_action=False)
|
||||
self.assertEqual(items['revenues']['data'][0]['to_invoice'], product_price, "The quantity to_invoice should be equal to the price of the product")
|
||||
|
||||
so_line.price_unit = 2*product_price
|
||||
items = project._get_profitability_items(with_action=False)
|
||||
self.assertEqual(items['revenues']['data'][0]['to_invoice'], 2*product_price, "The quantity to_invoice should be equal to twice the price of the product")
|
||||
|
||||
def test_sale_order_with_multiple_project_templates(self):
|
||||
"""Test when creating multiple projects for one sale order every project has its own allocated hours"""
|
||||
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
|
|
@ -860,7 +939,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': self.uom_hour.id,
|
||||
'uom_po_id': self.uom_hour.id,
|
||||
'default_code': 'c1',
|
||||
'service_tracking': 'task_in_project',
|
||||
'project_id': False, # will create a project,
|
||||
|
|
@ -872,7 +950,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': self.uom_hour.id,
|
||||
'uom_po_id': self.uom_hour.id,
|
||||
'default_code': 'c2',
|
||||
'service_tracking': 'task_in_project',
|
||||
'project_id': False, # will create a project,
|
||||
|
|
@ -886,14 +963,12 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
'name': product_1.name,
|
||||
'product_id': product_1.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': product_1.uom_id.id,
|
||||
'price_unit': product_1.list_price,
|
||||
}, {
|
||||
'order_id': sale_order.id,
|
||||
'name': product_2.name,
|
||||
'product_id': product_2.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': product_2.uom_id.id,
|
||||
'price_unit': product_2.list_price,
|
||||
},
|
||||
])
|
||||
|
|
@ -902,10 +977,349 @@ class TestSaleTimesheet(TestCommonSaleTimesheet):
|
|||
self.assertEqual(10, sale_order_line_template_1.project_id.allocated_hours)
|
||||
self.assertEqual(5, sale_order_line_template_2.project_id.allocated_hours)
|
||||
|
||||
def test_onchange_uom_service_product(self):
|
||||
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||
uom_kg = self.env.ref('uom.product_uom_kgm')
|
||||
|
||||
class TestSaleTimesheetView(TestCommonTimesheet):
|
||||
def test_get_view_timesheet_encode_uom(self):
|
||||
""" Test the label of timesheet time spent fields according to the company encoding timesheet uom """
|
||||
self.assert_get_view_timesheet_encode_uom([
|
||||
('sale_timesheet.project_project_view_form', '//field[@name="display_cost"]', [None, 'Daily Cost']),
|
||||
# Create product (consumable that will be switch to service)
|
||||
product_1 = self.env['product.template'].create([
|
||||
{
|
||||
'name': "Consumable to convert to service 1",
|
||||
'standard_price': 10,
|
||||
},
|
||||
])
|
||||
product_2 = self.env['product.product'].create({
|
||||
'name': "Consumable to convert to service 2",
|
||||
'standard_price': 15,
|
||||
})
|
||||
|
||||
# Initial uom should be unit
|
||||
self.assertEqual([product_1.uom_id.id, product_2.uom_id.id], [uom_unit.id]*2)
|
||||
products = [product_1, product_2] #perform the tests for both product and variants
|
||||
for product in products:
|
||||
# 1. product.template form: [uom: unit] --> change to service --> [uom: hour]
|
||||
with Form(product, view="sale_timesheet.view_product_timesheet_form") as product_form:
|
||||
product_form.type = 'service'
|
||||
product_form.service_policy = 'delivered_timesheet'
|
||||
self.assertEqual(product_form.uom_id.id, self.uom_hour.id)
|
||||
|
||||
# 2. product.template form: [uom: kgm] --> change to service --> [uom: hour] --> change to consumable --> [uom: kgm]
|
||||
product.write({
|
||||
'type': 'consu',
|
||||
'uom_id': uom_kg.id,
|
||||
})
|
||||
with Form(product, view="sale_timesheet.view_product_timesheet_form") as product_form:
|
||||
product_form.type = 'service'
|
||||
product_form.service_policy = 'delivered_timesheet'
|
||||
self.assertEqual(product_form.uom_id.id, self.uom_hour.id)
|
||||
product_form.type = 'consu'
|
||||
self.assertEqual(product_form.uom_id.id, uom_kg.id)
|
||||
|
||||
def test_allocated_hours_copy(self):
|
||||
""" This test ensures that the generated project's allocated_hours field is copied from the project template when it is set."""
|
||||
project_template = self.env['project.project'].create({
|
||||
'name': 'Template',
|
||||
'allocated_hours': 65,
|
||||
})
|
||||
product = self.env['product.product'].create({
|
||||
'name': "Service with template",
|
||||
'standard_price': 10,
|
||||
'list_price': 20,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': self.uom_hour.id,
|
||||
'default_code': 'c1',
|
||||
'service_tracking': 'task_in_project',
|
||||
'project_id': False, # will create a project,
|
||||
'project_template_id': project_template.id,
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': product.list_price,
|
||||
})
|
||||
project = sale_order_line._timesheet_create_project()
|
||||
self.assertTrue(
|
||||
project.allocated_hours == project_template.allocated_hours != sale_order_line.product_uom_qty,
|
||||
"The project's allocated hours should have been copied from its template, rather than the sale order line",
|
||||
)
|
||||
|
||||
def test_non_consolidated_billing_service_timesheet(self):
|
||||
"""
|
||||
When consolidated_billing is set to False, an invoice is created for each sale order
|
||||
Makes sure it works with sales orders linked to timesheets
|
||||
"""
|
||||
|
||||
sale_orders = self.env['sale.order'].create([{
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
})],
|
||||
}, {
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
})],
|
||||
}])
|
||||
sale_orders.action_confirm()
|
||||
|
||||
self.env['account.analytic.line'].create([{
|
||||
'name': 'Timesheet',
|
||||
'task_id': task.id,
|
||||
'project_id': task.project_id.id,
|
||||
'unit_amount': 2,
|
||||
'employee_id': self.employee_user.id,
|
||||
} for task in sale_orders.tasks_ids])
|
||||
|
||||
advance_payment = self.env['sale.advance.payment.inv'].with_context(active_ids=sale_orders.ids).create({
|
||||
'consolidated_billing': False,
|
||||
})
|
||||
|
||||
invoices = advance_payment._create_invoices(sale_orders)
|
||||
|
||||
self.assertEqual(len(invoices), 2, "The number of invoices created should be equal to the number of sales orders.")
|
||||
|
||||
def test_timesheet_with_negative_time_spent(self):
|
||||
""" Check the billable type of a timesheet with negative time spent """
|
||||
sale_order = self.env['sale.order'].create([{
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
})],
|
||||
}])
|
||||
sale_order.action_confirm()
|
||||
task1 = sale_order.tasks_ids
|
||||
timesheet = self.env['account.analytic.line'].create([
|
||||
{
|
||||
'name': 'Timesheet',
|
||||
'task_id': task1.id,
|
||||
'project_id': task1.project_id.id,
|
||||
'unit_amount': -1,
|
||||
'employee_id': self.employee_user.id,
|
||||
},
|
||||
])
|
||||
self.assertEqual(timesheet.timesheet_invoice_type, 'billable_time')
|
||||
|
||||
def test_linked_timesheet_after_invoice_reversal(self):
|
||||
"""Test that uneditable timesheet entries aren't linked to a reversed invoice form"""
|
||||
|
||||
# Full refund credit note
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
})
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 1,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
task = so_line.task_id
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Invoice Reversal',
|
||||
'project_id': task.project_id.id,
|
||||
'task_id': task.id,
|
||||
'unit_amount': 5,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
invoice = sale_order._create_invoices()[0]
|
||||
invoice.action_post()
|
||||
self.assertEqual(timesheet.timesheet_invoice_id, invoice, "Timesheet should be linked to the invoice")
|
||||
reversal_wizard = self.env['account.move.reversal'].with_context(
|
||||
active_model='account.move',
|
||||
active_ids=invoice.ids
|
||||
).create({
|
||||
'reason': 'full refund',
|
||||
'journal_id': invoice.journal_id.id,
|
||||
})
|
||||
reversal_wizard.modify_moves()
|
||||
self.assertFalse(timesheet.timesheet_invoice_id, "Timesheet should not be linked to the invoice after reversal")
|
||||
timesheet.write({'unit_amount': 7})
|
||||
self.assertEqual(timesheet.unit_amount, 7, "It Should be possible to edit timesheet after invoice reversal")
|
||||
|
||||
# Partial refund credit note
|
||||
sale_order2 = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
})
|
||||
so_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 1,
|
||||
'order_id': sale_order2.id,
|
||||
})
|
||||
so_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_timesheet3.id,
|
||||
'product_uom_qty': 1,
|
||||
'order_id': sale_order2.id,
|
||||
})
|
||||
sale_order2.action_confirm()
|
||||
task1 = so_line1.task_id
|
||||
task2 = so_line2.task_id
|
||||
timesheet1 = self.env['account.analytic.line'].create({
|
||||
'name': 'Timesheet Task 1',
|
||||
'project_id': task1.project_id.id,
|
||||
'task_id': task1.id,
|
||||
'unit_amount': 5,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
timesheet2 = self.env['account.analytic.line'].create({
|
||||
'name': 'Timesheet Task 2',
|
||||
'project_id': task2.project_id.id,
|
||||
'task_id': task2.id,
|
||||
'unit_amount': 5,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
invoice2 = sale_order2._create_invoices()[0]
|
||||
invoice2.action_post()
|
||||
self.assertEqual(timesheet1.timesheet_invoice_id, invoice2, "Timesheet1 should be linked to the invoice")
|
||||
self.assertEqual(timesheet2.timesheet_invoice_id, invoice2, "Timesheet2 should be linked to the invoice")
|
||||
|
||||
refund_wizard = self.env['account.move.reversal'].with_context(
|
||||
active_model='account.move',
|
||||
active_ids=invoice2.ids
|
||||
).create({
|
||||
'reason': 'partial refund',
|
||||
'journal_id': invoice2.journal_id.id,
|
||||
})
|
||||
refund_action = refund_wizard.refund_moves()
|
||||
credit_note = self.env['account.move'].browse(refund_action['res_id'])
|
||||
invoice_line_to_remove = credit_note.invoice_line_ids.filtered(
|
||||
lambda line: line.sale_line_ids.id == so_line2.id
|
||||
)
|
||||
invoice_line_to_remove.unlink()
|
||||
credit_note.action_post()
|
||||
self.assertFalse(timesheet1.timesheet_invoice_id, "Timesheet1 should be cleared after partial refund of its task")
|
||||
self.assertEqual(timesheet2.timesheet_invoice_id, invoice2, "Timesheet2 should still be linked to the original invoice")
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleTimesheetAnalyticPlan(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.plan_b, cls.plan_c = cls.env['account.analytic.plan'].create([
|
||||
{'name': 'plan 2'},
|
||||
{'name': 'plan 3'},
|
||||
])
|
||||
|
||||
def test_timesheet_get_accounts_from_sol(self):
|
||||
project_analytic_plan, _other_plans = self.env['account.analytic.plan']._get_all_plans()
|
||||
other_analytic_plan2 = self.plan_b
|
||||
analytic_account1, analytic_account2 = self.env['account.analytic.account'].create([
|
||||
{
|
||||
'name': 'Analytic Account 1',
|
||||
'plan_id': project_analytic_plan.id,
|
||||
},
|
||||
{
|
||||
'name': 'Analytic Account 2',
|
||||
'plan_id': other_analytic_plan2.id,
|
||||
},
|
||||
])
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'name': 'SO Test',
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
so_line1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet4.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
'analytic_distribution': {f'{analytic_account1.id}, {analytic_account2.id}': 100},
|
||||
})
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'project_id': self.project_global.id,
|
||||
'unit_amount': 50,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'so_line': so_line1.id,
|
||||
})
|
||||
self.assertEqual(timesheet.account_id, analytic_account1)
|
||||
self.assertEqual(timesheet[other_analytic_plan2._column_name()], analytic_account2)
|
||||
|
||||
# Create another analytic account and a new SOL to assign it to the timesheet
|
||||
other_analytic_plan3 = self.plan_c
|
||||
analytic_account3 = self.env['account.analytic.account'].create({
|
||||
'name': 'Analytic Account 3',
|
||||
'plan_id': other_analytic_plan3.id,
|
||||
})
|
||||
so_line2 = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet4.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
'analytic_distribution': {f'{analytic_account1.id}, {analytic_account3.id}': 100},
|
||||
})
|
||||
timesheet.so_line = so_line2
|
||||
self.assertEqual(timesheet.account_id, analytic_account1)
|
||||
self.assertFalse(timesheet[other_analytic_plan2._column_name()])
|
||||
self.assertEqual(timesheet[other_analytic_plan3._column_name()], analytic_account3)
|
||||
|
||||
def test_timesheet_get_accounts_from_sol_fallback_on_project(self):
|
||||
_project_analytic_plan, other_plans = self.env['account.analytic.plan']._get_all_plans()
|
||||
other_analytic_plan2 = self.plan_b
|
||||
analytic_account2 = self.env['account.analytic.account'].create({
|
||||
'name': 'Analytic Account 2',
|
||||
'plan_id': other_analytic_plan2.id,
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'name': 'SO Test',
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
so_project = sale_order.project_id
|
||||
so_line.analytic_distribution = {str(analytic_account2.id): 100}
|
||||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Timesheet',
|
||||
'project_id': so_project.id,
|
||||
'unit_amount': 1,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'so_line': so_line.id,
|
||||
})
|
||||
self.assertEqual(
|
||||
timesheet._get_analytic_accounts(),
|
||||
so_project.account_id | analytic_account2,
|
||||
"The analytic accounts should be the account_id from the project and the accounts from the SOL's distribution",
|
||||
)
|
||||
|
||||
def test_mandatory_plan_timesheet_applicability_from_sol(self):
|
||||
plan_a = self.analytic_plan
|
||||
plan_b = self.plan_b
|
||||
analytic_account, _dummy = self.env['account.analytic.account'].create([{
|
||||
'name': 'account',
|
||||
'plan_id': plan.id,
|
||||
} for plan in (plan_a, plan_b)])
|
||||
self.env['account.analytic.applicability'].create({
|
||||
'business_domain': 'timesheet',
|
||||
'applicability': 'mandatory',
|
||||
'analytic_plan_id': plan_b.id,
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'name': 'SO Test',
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet4.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
'analytic_distribution': {f'{analytic_account.id}': 100},
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
# The analytic plan 'other_analytic_plan' is mandatory on the sale order line linked to the timesheet
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'project_id': self.project_global.id,
|
||||
'unit_amount': 50,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'so_line': so_line.id,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,21 +9,19 @@ from odoo.exceptions import UserError
|
|||
class TestAccruedTimeSheetSaleOrders(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'pricelist_id': cls.company_data['default_pricelist'].id,
|
||||
'date_order': '2020-01-01',
|
||||
})
|
||||
so_line_deliver_global_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_delivery_timesheet2.name,
|
||||
'product_id': cls.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 50,
|
||||
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
|
||||
'price_unit': cls.product_delivery_timesheet2.list_price,
|
||||
'order_id': cls.sale_order.id,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.sale_project.tests.test_sale_project_dashboard import TestProjectDashboardCommon as Common
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleTimesheetDashboard(Common):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.dashboard_product_delivery_timesheet = cls.env['product.product'].create({
|
||||
'name': "Service delivered",
|
||||
'standard_price': 11,
|
||||
'list_price': 13,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI1',
|
||||
'service_type': 'timesheet',
|
||||
'service_tracking': 'no',
|
||||
'project_id': False,
|
||||
'taxes_id': False,
|
||||
})
|
||||
|
||||
def test_get_sale_item_data_various_sol_with_timesheet_installed(self):
|
||||
"""This test ensures that when the timesheet module is installed, the sols are computed and put into the new profitability sections."""
|
||||
hour_uom_id = self.env.ref('uom.product_uom_hour').id
|
||||
unit_uom_id = self.env.ref('uom.product_uom_unit').id
|
||||
|
||||
sols = self.dashboardSaleOrderLine.create([{
|
||||
'product_id': self.product_milestone.id,
|
||||
'product_uom_qty': 1,
|
||||
}, {
|
||||
'product_id': self.product_prepaid.id,
|
||||
'product_uom_qty': 1,
|
||||
}, {
|
||||
'product_id': self.material_product.id,
|
||||
'product_uom_qty': 1,
|
||||
}, {
|
||||
'product_id': self.dashboard_product_delivery_timesheet.id,
|
||||
'product_uom_qty': 1,
|
||||
}, {
|
||||
'product_id': self.dashboard_product_delivery_service.id,
|
||||
'product_uom_qty': 1,
|
||||
}])
|
||||
|
||||
sale_item_data = self.dashboard_project.get_sale_items_data(with_action=False, limit=5, section_id='materials')
|
||||
expected_dict = sols._read_format(
|
||||
['display_name', 'product_uom_qty', 'qty_delivered', 'qty_invoiced', 'product_uom_id', 'product_id']
|
||||
)
|
||||
self.assertEqual(
|
||||
sale_item_data['sol_items'][0],
|
||||
expected_dict[2]
|
||||
)
|
||||
sale_item_data = self.dashboard_project.get_sale_items_data(with_action=False, limit=5, section_id='billable_fixed')
|
||||
self.assertEqual(
|
||||
sale_item_data['sol_items'][0],
|
||||
expected_dict[1]
|
||||
)
|
||||
sale_item_data = self.dashboard_project.get_sale_items_data(with_action=False, limit=5, section_id='billable_milestones')
|
||||
self.assertEqual(
|
||||
sale_item_data['sol_items'][0],
|
||||
expected_dict[0]
|
||||
)
|
||||
sale_item_data = self.dashboard_project.get_sale_items_data(with_action=False, limit=5, section_id='billable_time')
|
||||
self.assertEqual(
|
||||
sale_item_data['sol_items'][0],
|
||||
expected_dict[3]
|
||||
)
|
||||
sale_item_data = self.dashboard_project.get_sale_items_data(with_action=False, limit=5, section_id='billable_manual')
|
||||
self.assertEqual(
|
||||
sale_item_data['sol_items'][0],
|
||||
expected_dict[4]
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProductProduct(TestCommonSaleTimesheet):
|
||||
def test_delete_master_timesheet_product(self):
|
||||
"""
|
||||
Test that the master timesheet product cannot be deleted, archived nor linked
|
||||
to a company but regular products still can.
|
||||
"""
|
||||
time_product = self.env.ref('sale_timesheet.time_product')
|
||||
with self.assertRaises(ValidationError):
|
||||
time_product._unlink_except_master_data()
|
||||
with self.assertRaises(ValidationError):
|
||||
time_product.write({'active': False})
|
||||
with self.assertRaises(ValidationError):
|
||||
time_product.write({'company_id': self.env.company.id})
|
||||
|
||||
non_timesheet_product = self.product_delivery_timesheet5
|
||||
non_timesheet_product.write({'company_id': self.env.company.id})
|
||||
self.assertEqual(non_timesheet_product.company_id, self.env.company)
|
||||
non_timesheet_product.write({'active': False})
|
||||
self.assertFalse(non_timesheet_product.active)
|
||||
non_timesheet_product._unlink_except_master_data()
|
||||
self.assertFalse(self.env['product.product'].search([('id', '=', non_timesheet_product.id)]))
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProductTemplate(TestCommonSaleTimesheet):
|
||||
def test_delete_master_timesheet_product(self):
|
||||
"""
|
||||
Test that the master timesheet product cannot be deleted, archived nor linked
|
||||
to a company but regular products still can.
|
||||
"""
|
||||
time_product = self.env.ref('sale_timesheet.time_product').product_tmpl_id
|
||||
with self.assertRaises(ValidationError):
|
||||
time_product.unlink()
|
||||
with self.assertRaises(ValidationError):
|
||||
time_product.write({'active': False})
|
||||
with self.assertRaises(ValidationError):
|
||||
time_product.write({'company_id': self.env.company.id})
|
||||
|
||||
non_timesheet_product = self.product_delivery_timesheet5.product_tmpl_id
|
||||
non_timesheet_product.write({'company_id': self.env.company.id})
|
||||
self.assertEqual(non_timesheet_product.company_id, self.env.company)
|
||||
non_timesheet_product.write({'active': False})
|
||||
self.assertFalse(non_timesheet_product.active)
|
||||
non_timesheet_product.unlink()
|
||||
self.assertFalse(self.env['product.template'].browse(non_timesheet_product.id).exists())
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from odoo.tests import tagged
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleTimesheetReport(TestCommonSaleTimesheet):
|
||||
|
||||
def test_timesheet_report_uom_difference(self):
|
||||
'''Test the timesheet revenue are correctly computed in case of a
|
||||
fixed price service
|
||||
'''
|
||||
uom_days = self.env.ref('uom.product_uom_day')
|
||||
|
||||
# Change product uom from hours to days
|
||||
self.product_order_timesheet3.write({
|
||||
'uom_id': uom_days,
|
||||
})
|
||||
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
})
|
||||
so_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom_qty': 3,
|
||||
'order_id': sale_order.id,
|
||||
'price_unit': 10.0,
|
||||
'tax_ids': [Command.set(self.tax_sale_a.ids)],
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
task = self.env['project.task'].search([('sale_line_id', '=', so_line.id)])
|
||||
|
||||
# Register a timesheet entry of 24 hours
|
||||
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,
|
||||
'so_line': so_line.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
self.env.flush_all()
|
||||
|
||||
report = self.env['timesheets.analysis.report'].search([('so_line', '=', so_line.id)])
|
||||
self.assertEqual(report.unit_amount, 24)
|
||||
self.assertEqual(report.timesheet_revenues, 30)
|
||||
self.assertEqual(report.billable_time, 24)
|
||||
|
|
@ -9,37 +9,29 @@ _logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestUi(HttpCase):
|
||||
class TestSaleTimesheetUi(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
service_category_id = cls.env['product.category'].create({
|
||||
'name': 'Services',
|
||||
'parent_id': cls.env.ref('product.product_category_1').id,
|
||||
}).id
|
||||
|
||||
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': service_category_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_prepaid',
|
||||
'service_tracking': 'no',
|
||||
})
|
||||
|
||||
# Enable the "Milestones" feature to be able to create milestones on this tour.
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
cls.env.ref('base.group_user').sudo().implied_ids |= cls.env.ref('project.group_project_milestone')
|
||||
|
||||
admin = cls.env.ref('base.user_admin')
|
||||
admin.employee_id.hourly_cost = 75
|
||||
|
||||
def test_ui(self):
|
||||
self.start_tour('/web', 'sale_timesheet_tour', login='admin', timeout=100)
|
||||
self.env['product.pricelist'].with_context(active_test=False).search([]).unlink()
|
||||
self.start_tour('/odoo', 'sale_timesheet_tour', login='admin', timeout=100)
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ class TestSoLineDeterminedInTimesheet(TestCommonSaleTimesheet):
|
|||
timesheet = self.env['account.analytic.line'].create({
|
||||
'name': 'Test Line',
|
||||
'unit_amount': 1,
|
||||
'auto_account_id': self.analytic_account_sale.id,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': self.project_employee_rate.id,
|
||||
'task_id': task.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from odoo.tests import tagged
|
||||
|
||||
from .common import TestCommonSaleTimesheet
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSaleTimesheetTaskAnalysis(TestCommonSaleTimesheet):
|
||||
def test_remaining_hours_so(self):
|
||||
sales_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
})
|
||||
so_line_1 = self.env['sale.order.line'].create({
|
||||
'product_id': self.product_order_timesheet3.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sales_order.id,
|
||||
})
|
||||
sales_order.action_confirm()
|
||||
|
||||
task_1 = self.env['project.task'].search([('sale_line_id', '=', so_line_1.id)])
|
||||
task_2 = self.env['project.task'].create({
|
||||
'name': "Task 2",
|
||||
'project_id': task_1.project_id.id,
|
||||
'partner_id': self.partner_a.id,
|
||||
'sale_line_id': so_line_1.id,
|
||||
})
|
||||
self.assertEqual(task_2.remaining_hours_so, 10)
|
||||
|
||||
self.env.flush_all()
|
||||
task_report = self.env['report.project.task.user'].search([('task_id', '=', task_2.id)])
|
||||
self.assertEqual(task_report.remaining_hours_so, task_2.remaining_hours_so)
|
||||
|
|
@ -21,7 +21,7 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
2) Create SO with a SOL containing this updated product,
|
||||
3) Create Project and Task,
|
||||
4) Timesheet in the task to satisfy the condition for the SOL to display an upsell warning,
|
||||
5) Check if the SO has an 'sale.mail_act_sale_upsell' activity.
|
||||
5) Check if the SO has an 'mail.mail_activity_data_todo' activity.
|
||||
"""
|
||||
# 1) Configure the upsell warning in prepaid service product
|
||||
self.product_order_timesheet1.write({
|
||||
|
|
@ -48,7 +48,7 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'partner_id': self.partner_a.id,
|
||||
'analytic_account_id': self.analytic_account_sale.id,
|
||||
'account_id': self.analytic_account_sale.id,
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task Test',
|
||||
|
|
@ -71,7 +71,7 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
# Normally this method is called at the end of _compute_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
|
||||
self.assertEqual(len(so.activity_search(['sale.mail_act_sale_upsell'])), 0, 'No upsell warning should appear in the SO.')
|
||||
self.assertEqual(len(so.activity_search(['mail.mail_activity_data_todo'])), 0, 'No upsell warning should appear in the SO.')
|
||||
timesheet.write({
|
||||
'unit_amount': 6,
|
||||
})
|
||||
|
|
@ -82,8 +82,8 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
# Normally this method is called at the end of _compute_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
|
||||
# 5) Check if the SO has an 'sale.mail_act_sale_upsell' activity.
|
||||
self.assertEqual(len(so.activity_search(['sale.mail_act_sale_upsell'])), 1, 'A upsell warning should appear in the SO.')
|
||||
# 5) Check if the SO has an 'mail.mail_activity_data_todo' activity.
|
||||
self.assertEqual(len(so.activity_search(['mail.mail_activity_data_todo'])), 1, 'A upsell warning should appear in the SO.')
|
||||
|
||||
def test_display_upsell_warning_when_invoiced(self):
|
||||
""" Test to display an upsell warning when threshold value (10000%) exceed while creating invoice.
|
||||
|
|
@ -98,7 +98,7 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
3) Create Project and Task,
|
||||
4) Timesheet in the task to satisfy the condition for the SOL to display an upsell warning,
|
||||
5) Create Invoice of the SO,
|
||||
6) Check if the SO has an 'sale.mail_act_sale_upsell' activity.
|
||||
6) Check if the SO has an 'mail.mail_activity_data_todo' activity.
|
||||
"""
|
||||
|
||||
# 1) Configure the upsell warning in prepaid service product with 100 (10000%)
|
||||
|
|
@ -128,7 +128,7 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'partner_id': self.partner_a.id,
|
||||
'analytic_account_id': self.analytic_account_sale.id,
|
||||
'account_id': self.analytic_account_sale.id,
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task Test',
|
||||
|
|
@ -151,5 +151,100 @@ class TestUpsellWarning(TestCommonSaleTimesheet):
|
|||
# Normally this method is called at the end of _get_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
|
||||
# 6) Check if the SO has an 'sale.mail_act_sale_upsell' activity.
|
||||
self.assertEqual(len(so.activity_search(['sale.mail_act_sale_upsell'])), 0, 'No upsell warning should appear in the SO.')
|
||||
# 6) Check if the SO has an 'mail.mail_activity_data_todo' activity.
|
||||
self.assertEqual(len(so.activity_search(['mail.mail_activity_data_todo'])), 0, 'No upsell warning should appear in the SO.')
|
||||
|
||||
def test_display_upsell_warning_multiple_times(self):
|
||||
""" Test to display an upsell warning caused by an SO line that has already produced an upsell warning previously.
|
||||
|
||||
We display an upsell warning in SO when this following condition is satisfy in its SOL:
|
||||
(qty_delivered / product_uom_qty) >= product_id.service_upsell_threshold
|
||||
|
||||
Test Case:
|
||||
=========
|
||||
1) Configure the upsell warning in prepaid service product
|
||||
2) Create SO with a SOL containing this updated product,
|
||||
3) Create Project and Task,
|
||||
4) Timesheet in the task to satisfy the condition for the SOL to display an upsell warning,
|
||||
5) Update the ordered quantity of the SOL to match its delivered quantity
|
||||
6) Mark the upsell activity as done,
|
||||
7) Create Invoice of the SO,
|
||||
8) Timesheet again in the task to satisfy the condition for the SOL to display an upsell warning,
|
||||
9) Check if the SO has an 'mail.mail_activity_data_todo' activity.
|
||||
"""
|
||||
|
||||
# 1) Configure the upsell warning in prepaid service product
|
||||
self.product_order_timesheet1.write({
|
||||
'service_upsell_threshold': 1.0,
|
||||
})
|
||||
|
||||
# 2) Create SO with a SOL containing this updated product
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
})
|
||||
|
||||
self.env['sale.order.line'].create({
|
||||
'order_id': so.id,
|
||||
'product_id': self.product_order_timesheet1.id,
|
||||
'product_uom_qty': 10,
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
# 3) Create Project and Task
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Project',
|
||||
'allow_timesheets': True,
|
||||
'allow_billable': True,
|
||||
'partner_id': self.partner_a.id,
|
||||
'account_id': self.analytic_account_sale.id,
|
||||
})
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task Test',
|
||||
'project_id': project.id,
|
||||
})
|
||||
task._compute_sale_line()
|
||||
|
||||
# 4) Timesheet in the task to satisfy the condition for the SOL to display an upsell warning
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': 'Timesheet1',
|
||||
'unit_amount': 15,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': project.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
so.order_line._compute_qty_delivered()
|
||||
# Normally this method is called at the end of _get_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
self.assertEqual(len(so.activity_search(['mail.mail_activity_data_todo'])), 1, 'An upsell warning should appear in the SO.')
|
||||
|
||||
# 5) Update the ordered quantity of the SOL to match its delivered quantity
|
||||
so.order_line.write({
|
||||
'product_uom_qty': so.order_line.qty_delivered,
|
||||
})
|
||||
|
||||
# 6) Mark the upsell activity as done
|
||||
so.activity_search(['mail.mail_activity_data_todo'])._action_done()
|
||||
|
||||
# 7) Create Invoice of the SO
|
||||
so._create_invoices()
|
||||
# Normally this method is called at the end of _get_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
# No 'mail.mail_activity_data_todo' activity should appear as it was marked as done
|
||||
self.assertEqual(len(so.activity_search(['mail.mail_activity_data_todo'])), 0, 'No upsell warning should appear in the SO.')
|
||||
|
||||
# 8) Timesheet again in the task to satisfy the condition for the SOL to display an upsell warning
|
||||
self.env['account.analytic.line'].create({
|
||||
'name': 'Timesheet2',
|
||||
'unit_amount': 5,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'project_id': project.id,
|
||||
'task_id': task.id,
|
||||
})
|
||||
so.order_line._compute_qty_delivered()
|
||||
# Normally this method is called at the end of _get_invoice_status and other compute method. Here, we simulate for invoice_status field
|
||||
so._compute_field_value(so._fields['invoice_status'])
|
||||
|
||||
# 9) Check if the SO has an 'mail.mail_activity_data_todo' activity
|
||||
self.assertEqual(len(so.activity_search(['mail.mail_activity_data_todo'])), 1, 'A upsell warning should appear in the SO.')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue