19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_analytic_distribution
from . import test_child_tasks
from . import test_project_profitability
from . import test_res_config_settings
from . import test_project_project
from . import test_reinvoice
from . import test_sale_project
from . import test_so_line_milestones
from . import test_sale_project_dashboard
from . import test_sale_project_multicompany_access

View file

@ -6,20 +6,16 @@ from odoo.addons.sale.tests.common import TestSaleCommon
class TestSaleProjectCommon(TestSaleCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.env.user.group_ids += cls.quick_ref('project.group_project_manager')
cls.env['res.config.settings'] \
.create({'group_project_milestone': True}) \
.execute()
cls.env.user.group_ids |= cls.env.ref('project.group_project_milestone')
cls.uom_hour = cls.env.ref('uom.product_uom_hour')
cls.account_sale = cls.company_data['default_account_revenue']
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan Test',
'company_id': cls.company_data['company'].id,
})
cls.analytic_plan, _other_plans = cls.env['account.analytic.plan']._get_all_plans()
cls.analytic_account_sale = cls.env['account.analytic.account'].create({
'name': 'Project for selling timesheet - AA',
'code': 'AA-2030',
@ -29,7 +25,7 @@ class TestSaleProjectCommon(TestSaleCommon):
Project = cls.env['project.project'].with_context(tracking_disable=True)
cls.project_global = Project.create({
'name': 'Project Global',
'analytic_account_id': cls.analytic_account_sale.id,
'account_id': cls.analytic_account_sale.id,
'allow_billable': True,
})
cls.project_template = Project.create({
@ -49,7 +45,6 @@ class TestSaleProjectCommon(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': cls.uom_hour.id,
'uom_po_id': cls.uom_hour.id,
'default_code': 'SERV-DELI1',
'service_type': 'manual',
'service_tracking': 'no',
@ -64,7 +59,6 @@ class TestSaleProjectCommon(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': cls.uom_hour.id,
'uom_po_id': cls.uom_hour.id,
'default_code': 'SERV-DELI2',
'service_type': 'manual',
'service_tracking': 'task_global_project',
@ -79,7 +73,6 @@ class TestSaleProjectCommon(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': cls.uom_hour.id,
'uom_po_id': cls.uom_hour.id,
'default_code': 'SERV-DELI3',
'service_type': 'manual',
'service_tracking': 'task_in_project',
@ -94,7 +87,6 @@ class TestSaleProjectCommon(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': cls.uom_hour.id,
'uom_po_id': cls.uom_hour.id,
'default_code': 'SERV-DELI4',
'service_type': 'manual',
'service_tracking': 'project_only',
@ -109,7 +101,6 @@ class TestSaleProjectCommon(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': cls.uom_hour.id,
'uom_po_id': cls.uom_hour.id,
'default_code': 'SERV-DELI4',
'service_type': 'manual',
'service_tracking': 'project_only',
@ -118,13 +109,49 @@ class TestSaleProjectCommon(TestSaleCommon):
'taxes_id': False,
'property_account_income_id': cls.account_sale.id,
})
price_vals = {
'standard_price': 11,
'list_price': 13,
}
service_vals = {
'type': 'service',
'service_tracking': 'no',
'project_id': False,
}
(
cls.product_service_ordered_prepaid,
cls.product_service_delivered_milestone,
cls.product_service_delivered_manual,
cls.product_consumable,
) = cls.env['product.product'].create([{
'name': "Service prepaid",
**price_vals,
**service_vals,
'invoice_policy': 'order',
'service_type': 'manual',
}, {
'name': "Service milestone",
**price_vals,
**service_vals,
'invoice_policy': 'delivery',
'service_type': 'milestones',
}, {
'name': "Service manual",
**price_vals,
**service_vals,
'invoice_policy': 'delivery',
'service_type': 'manual',
}, {
'name': "Consumable",
**price_vals,
'type': 'consu',
'invoice_policy': 'order',
}])
# -- devliered_milestones (delivered, milestones)
product_milestone_vals = {
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': cls.uom_hour.id,
'uom_po_id': cls.uom_hour.id,
'default_code': 'SERV-MILES',
'service_type': 'milestones',
'service_tracking': 'no',
@ -134,8 +161,3 @@ class TestSaleProjectCommon(TestSaleCommon):
{**product_milestone_vals, 'name': 'Milestone Product', 'list_price': 20},
{**product_milestone_vals, 'name': 'Milestone Product 2', 'list_price': 15},
])
def set_project_milestone_feature(self, value):
self.env['res.config.settings'] \
.create({'group_project_milestone': value}) \
.execute()

View file

@ -0,0 +1,139 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from .common import TestSaleProjectCommon
from odoo.fields import Command, Domain
from odoo.tests import HttpCase
from odoo.tests.common import tagged
@tagged('post_install', '-at_install')
class TestAnalyticDistribution(HttpCase, TestSaleProjectCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Creating analytic plans within tests could cause some registry issues
# hence we are creating them in the setupClass instead.
# This is because creating a plan creates fields and columns on models inheriting
# from the mixin.
# The registry is reset on class cleanup.
cls.plan_b = cls.env['account.analytic.plan'].create({'name': 'Q'})
def test_project_transmits_analytic_plans_to_sol_distribution(self):
plan_a = self.analytic_plan
plan_b = self.plan_b
account_a, account_b = self.env['account.analytic.account'].create([{
'name': 'account',
'plan_id': plan.id,
} for plan in (plan_a, plan_b)])
project = self.env['project.project'].create({
'name': 'X',
plan_a._column_name(): account_a.id,
plan_b._column_name(): account_b.id,
})
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'project_id': project.id,
})
sale_order_line = self.env['sale.order.line'].create({
'order_id': sale_order.id,
'product_id': self.product.id,
})
self.assertEqual(
sale_order_line.analytic_distribution,
{f'{account_a.id},{account_b.id}': 100},
"The sale order line's analytic distribution should have one line containing all the accounts of the project's plans"
)
def test_sol_analytic_distribution_project_template_service(self):
sale_order = self.env['sale.order'].create({'partner_id': self.partner.id})
sale_order_line = self.env['sale.order.line'].create({
'order_id': sale_order.id,
'product_id': self.product_delivery_manual5.id,
})
self.assertFalse(
sale_order_line.analytic_distribution,
"No default analytic distribution should be set on the SOL as no project is linked to the SO, and we do not "
"take the project template set on the product into account.",
)
sale_order.action_confirm()
self.assertEqual(
sale_order_line.analytic_distribution,
{str(sale_order.project_id.account_id.id): 100},
"The analytic distribution of the SOL should be set to the account of the generated project.",
)
def test_sol_analytic_distribution_task_in_project_service(self):
self.project_global.account_id = self.analytic_account_sale
sale_order = self.env['sale.order'].create({'partner_id': self.partner.id})
sale_order_line = self.env['sale.order.line'].create({
'order_id': sale_order.id,
'product_id': self.product_delivery_manual2.id,
})
self.assertEqual(
sale_order_line.analytic_distribution,
{str(self.project_global.account_id.id): 100},
"The analytic distribution of the SOL should be set to the account of the project set on the product.",
)
def test_project_analytic_distribution_on_invoice_lines(self):
"""
Test that Analytic Distribution applies from Project to Invoice Lines (excluding payable/receivable lines).
Steps:
1. Create a project.
2. Create an invoice with the project in context.
3. Add an invoice line.
4. Verify analytic distribution is applied.
"""
invoice = self.env['account.move'].with_context({
'default_move_type': 'out_invoice',
'default_partner_id': self.project_global.partner_id.id,
'project_id': self.project_global.id
}).create({
'invoice_line_ids': [Command.create({
'product_id': self.product_delivery_manual1.id,
'quantity': 1,
'price_unit': 10,
})]
})
filtered_lines = invoice.line_ids.filtered(lambda l: l.analytic_distribution)
self.assertEqual(
len(filtered_lines),
1,
"Analytic distribution is not set on the payable/receivable lines"
)
def test_get_so_mapping_domain_with_no_analytic_distribution(self):
"""
Ensure _get_so_mapping_domain doesnt fail when analytic_distribution is not set
"""
account = self.env['account.account'].create({
'name': 'Receivable test account',
'code': '00001',
'account_type': 'asset_receivable',
})
move = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
})
line = self.env['account.move.line'].create({
'move_id': move.id,
'name': 'Line without analytic',
'quantity': 1,
'price_unit': 100,
'account_id': account.id,
})
domain = line._get_so_mapping_domain()
self.assertEqual(
domain,
Domain.FALSE,
"Domain should be False when analytic_distribution is missing."
)

View file

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo import Command
from odoo.tests.common import TransactionCase, new_test_user
@ -19,12 +21,18 @@ class TestNestedTaskUpdate(TransactionCase):
product = cls.env['product.product'].create({
'name': "Prepaid Consulting",
'type': 'service',
'service_tracking': 'project_only',
})
cls.order_line = cls.env['sale.order.line'].create({
'name': "Order line",
'product_id': product.id,
'order_id': sale_order.id,
})
sale_order.action_confirm()
cls.project = cls.order_line.project_id
cls.project.reinvoiced_sale_order_id = False
cls.project.sale_line_id = False
cls.user = new_test_user(cls.env, login='mla')
#----------------------------------
@ -33,42 +41,46 @@ class TestNestedTaskUpdate(TransactionCase):
#
#----------------------------------
def test_default_values_creating_subtask(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [Command.link(self.user.id)], 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'project_id': self.project.id})
self.assertTrue(parent.allow_billable, "The parent task should be billable as the project linked is billable")
self.assertEqual(parent.partner_id, self.project.partner_id, "The partner set on the parent task should the one set on the project linked")
self.assertEqual(child.project_id, parent.project_id, "The project set on the subtask be inheritted from parent")
self.assertTrue(child.allow_billable, "The subtask should be billable since its parent task's project is billable")
self.assertEqual(child.partner_id, self.project.partner_id, "The partner set on the subtask should the one set on the project linked to the parent")
def test_creating_subtask_user_id_on_parent_dont_go_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False})
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)], 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False, 'project_id': self.project.id})
self.assertFalse(child.user_ids)
def test_creating_subtask_partner_id_on_parent_goes_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'project_id': self.project.id})
child._compute_partner_id() # the compute will be triggered since the user set the parent_id.
self.assertEqual(child.partner_id, self.user.partner_id)
# Another case, it is the parent as a default value
child = self.env['project.task'].with_context(default_parent_id=parent.id).create({'name': 'child'})
child = self.env['project.task'].with_context(default_parent_id=parent.id, default_project_id=self.project.id).create({'name': 'child'})
self.assertEqual(child.partner_id, self.user.partner_id)
def test_creating_subtask_email_from_on_parent_goes_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'email_from': 'a@c.be'})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertEqual(child.email_from, 'a@c.be')
def test_creating_subtask_sale_line_id_on_parent_goes_on_child_if_same_partner_in_values(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id, 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id, 'parent_id': parent.id, 'project_id': self.project.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
parent.write({'sale_line_id': False})
self.assertEqual(child.sale_line_id, self.order_line)
def test_creating_subtask_sale_line_id_on_parent_goes_on_child_with_partner_if_not_in_values(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'project_id': self.project.id})
self.assertEqual(child.partner_id, parent.partner_id)
self.assertEqual(child.sale_line_id, parent.sale_line_id)
def test_creating_subtask_sale_line_id_on_parent_dont_go_on_child_if_other_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id, 'project_id': self.project.id})
self.assertFalse(child.sale_line_id)
self.assertNotEqual(child.partner_id, parent.partner_id)
@ -76,8 +88,8 @@ class TestNestedTaskUpdate(TransactionCase):
commercial_partner = self.env['res.partner'].create({'name': "Jémémy"})
self.partner.parent_id = commercial_partner
self.user.partner_id.parent_id = commercial_partner
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id, 'project_id': self.project.id})
self.assertEqual(child.sale_line_id, self.order_line, "Sale order line on parent should be transfered to child")
self.assertNotEqual(child.partner_id, parent.partner_id)
@ -88,8 +100,8 @@ class TestNestedTaskUpdate(TransactionCase):
#----------------------------------------
def test_write_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False})
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id, 'project_id': self.project.id})
self.assertFalse(child.user_ids)
parent.write({'user_ids': [(4, self.user.id)]})
self.assertFalse(child.user_ids)
@ -97,26 +109,22 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertFalse(child.user_ids)
def test_write_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False, 'project_id': self.project.id})
child = self.env['project.task'].create({
'name': 'child',
'partner_id': False,
'parent_id': parent.id,
'project_id': self.env['project.project'].create({'name': 'proute'}).id,
})
self.assertFalse(child.partner_id)
parent.write({'partner_id': self.user.partner_id.id})
self.assertNotEqual(child.partner_id, parent.partner_id)
parent.write({'partner_id': False})
self.assertNotEqual(child.partner_id, self.user.partner_id)
def test_write_email_from_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent'})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
self.assertFalse(child.email_from)
parent.write({'email_from': 'a@c.be'})
self.assertEqual(child.email_from, parent.email_from)
parent.write({'email_from': ''})
self.assertEqual(child.email_from, 'a@c.be')
def test_write_sale_line_id_on_parent_write_on_child_if_same_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.partner.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.partner.id, 'project_id': self.project.id})
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
@ -124,8 +132,8 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, self.order_line)
def test_write_sale_line_id_on_parent_write_on_child_with_partner_if_not_set(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'project_id': self.project.id})
child._compute_partner_id()
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
@ -135,8 +143,8 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, self.order_line)
def test_write_sale_line_id_on_parent_dont_write_on_child_if_other_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.user.partner_id.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.user.partner_id.id, 'project_id': self.project.id})
self.assertFalse(child.sale_line_id)
parent.write({'sale_line_id': self.order_line.id})
self.assertFalse(child.sale_line_id)
@ -148,29 +156,22 @@ class TestNestedTaskUpdate(TransactionCase):
#----------------------------------
def test_linking_user_id_on_parent_dont_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
child = self.env['project.task'].create({'name': 'child', 'user_ids': False})
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)], 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'project_id': self.project.id})
self.assertFalse(child.user_ids)
child.write({'parent_id': parent.id})
self.assertFalse(child.user_ids)
def test_linking_partner_id_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'project_id': self.project.id})
self.assertFalse(child.partner_id)
child.write({'parent_id': parent.id})
child.write({'parent_id': parent.id, 'project_id': self.project.id})
self.assertEqual(child.partner_id, self.user.partner_id)
def test_linking_email_from_on_parent_write_on_child(self):
parent = self.env['project.task'].create({'name': 'parent', 'email_from': 'a@c.be'})
child = self.env['project.task'].create({'name': 'child', 'email_from': False})
self.assertFalse(child.email_from)
child.write({'parent_id': parent.id})
self.assertEqual(child.email_from, 'a@c.be')
def test_linking_sale_line_id_on_parent_write_on_child_if_same_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id, 'project_id': self.project.id})
self.assertFalse(child.sale_line_id)
child.write({'parent_id': parent.id})
self.assertEqual(child.sale_line_id, parent.sale_line_id)
@ -178,8 +179,8 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, self.order_line)
def test_linking_sale_line_id_on_parent_write_on_child_with_partner_if_not_set(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'project_id': self.project.id})
self.assertFalse(child.sale_line_id)
self.assertFalse(child.partner_id)
child.write({'parent_id': parent.id})
@ -187,16 +188,16 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, parent.sale_line_id)
def test_linking_sale_line_id_on_parent_write_dont_child_if_other_partner(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id})
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'project_id': self.project.id})
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'project_id': self.project.id})
self.assertFalse(child.sale_line_id)
self.assertNotEqual(child.partner_id, parent.partner_id)
child.write({'parent_id': parent.id})
self.assertFalse(child.sale_line_id)
def test_writing_on_parent_with_multiple_tasks(self):
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'partner_id': self.partner.id})
children_values = [{'name': 'child%s' % i, 'user_ids': False, 'parent_id': parent.id} for i in range(5)]
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'partner_id': self.partner.id, 'project_id': self.project.id})
children_values = [{'name': 'child%s' % i, 'user_ids': False, 'parent_id': parent.id, 'project_id': self.project.id} for i in range(5)]
children = self.env['project.task'].create(children_values)
children._compute_partner_id()
# test writing sale_line_id
@ -207,8 +208,8 @@ class TestNestedTaskUpdate(TransactionCase):
self.assertEqual(child.sale_line_id, self.order_line)
def test_linking_on_parent_with_multiple_tasks(self):
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'user_ids': [(4, self.user.id)]})
children_values = [{'name': 'child%s' % i, 'user_ids': False} for i in range(5)]
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'user_ids': [(4, self.user.id)], 'project_id': self.project.id})
children_values = [{'name': 'child%s' % i, 'user_ids': False, 'project_id': self.project.id} for i in range(5)]
children = self.env['project.task'].create(children_values)
# test writing user_ids and sale_line_id
@ -221,3 +222,70 @@ class TestNestedTaskUpdate(TransactionCase):
for child in children:
self.assertEqual(child.sale_line_id, self.order_line)
self.assertFalse(child.user_ids)
def test_allow_billable_on_subtasks(self):
parent = self.env['project.task'].create({
'name': 'Parent Task',
'project_id': self.project.id,
'child_ids': [
Command.create({
'name': 'Subtask 1',
'project_id': self.project.id,
}),
Command.create({
'name': 'Subtask 2',
'project_id': self.project.id,
'child_ids': [
Command.create({'name': 'Subsubtask', 'project_id': self.project.id})
],
}),
],
})
self.assertTrue(all((parent + parent._get_all_subtasks()).mapped('allow_billable')))
subtask2 = parent.child_ids.filtered(lambda t: t.name == 'Subtask 2')
subsubtask = subtask2.child_ids
project_non_billable = self.env['project.project'].create({'name': 'Non-billable project', 'allow_billable': False})
subtask2.project_id = project_non_billable
self.assertFalse(subtask2.allow_billable)
self.assertFalse(subsubtask.allow_billable)
# ----------------------------------
#
# When copying a project template, some values go on the child
#
# ----------------------------------
def test_associate_copied_task_to_copied_project(self):
"""
When confirming an SO with a product generating a project from a template,
check that the copied task and subtask are correctly assigned to the copied
project rather than its template.
"""
project_tempalte = self.env['project.project'].create({'name': 'Super Project'})
parent = self.env['project.task'].create({'name': 'parent task', 'project_id': project_tempalte.id})
child = self.env['project.task'].create({'name': 'child task', 'parent_id': parent.id, 'project_id': project_tempalte.id})
super_product = self.env['product.product'].create({
'name': 'Super product',
'type': 'service',
'service_tracking': 'project_only',
'project_template_id': project_tempalte.id,
})
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [
Command.create({
'name': super_product.name,
'product_id': super_product.id,
'product_uom_qty': 1,
'price_unit': 100,
})
]
})
sale_order.action_confirm()
self.assertEqual(project_tempalte.tasks, parent | child)
super_project = sale_order.order_line.project_id
self.assertFalse(super_project.tasks & project_tempalte.tasks)
self.assertEqual(len(super_project.tasks), 2)
self.assertEqual(super_project.tasks.parent_id, super_project.tasks.child_ids.parent_id)

View file

@ -0,0 +1,31 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged('-at_install', 'post_install')
class TestProjectProject(TransactionCase):
def test_projects_to_make_billable(self):
""" Test the projects fetched in the post init are the ones expected """
Project = self.env['project.project']
Task = self.env['project.task']
partner = self.env['res.partner'].create({'name': "Mur en béton"})
project1, project2, project3 = Project.create([
{'name': 'Project with partner', 'partner_id': partner.id, 'allow_billable': False},
{'name': 'Project without partner', 'allow_billable': False},
{'name': 'Project without partner 2', 'allow_billable': False},
])
Task.create([
{'name': 'Task with partner in project 2', 'project_id': project2.id, 'partner_id': partner.id},
{'name': 'Task without partner in project 2', 'project_id': project2.id},
{'name': 'Task without partner in project 3', 'project_id': project3.id},
])
projects_to_make_billable = Project.search(Project._get_projects_to_make_billable_domain())
non_billable_projects, = Task._read_group(
Task._get_projects_to_make_billable_domain([('project_id', 'not in', projects_to_make_billable.ids)]),
[],
['project_id:recordset'],
)[0]
projects_to_make_billable += non_billable_projects
self.assertEqual(projects_to_make_billable, project1 + project2)

View file

@ -0,0 +1,411 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.tests import Form, tagged
from odoo.fields import Command
@tagged('post_install', '-at_install')
class TestReInvoice(TestSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.group_ids += cls.quick_ref('project.group_project_manager')
cls.analytic_plan = cls.env['account.analytic.plan'].create({
'name': 'Plan',
})
cls.analytic_account = cls.env['account.analytic.account'].create({
'name': 'Test AA',
'code': 'TESTSALE_REINVOICE',
'company_id': cls.partner_a.company_id.id,
'plan_id': cls.analytic_plan.id,
'partner_id': cls.partner_a.id
})
cls.project = cls.env['project.project'].create({
'name': 'SO Project',
f'{cls.analytic_plan._column_name()}': cls.analytic_account.id,
})
# Remove the analytic account auto-generated when creating a timesheetable project if it exists
cls.project.account_id = False
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_a.id,
'partner_invoice_id': cls.partner_a.id,
'partner_shipping_id': cls.partner_a.id,
'project_id': cls.project.id,
})
cls.AccountMove = 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):
# Required for `analytic_distribution` to be visible in the view
self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')
""" Test vendor bill at cost for product based on ordered and delivered quantities. """
# create SO line and confirm SO (with only one line)
sale_order_line1 = self.env['sale.order.line'].create({
'product_id': self.company_data['product_order_cost'].id,
'product_uom_qty': 2,
'qty_delivered': 1,
'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,
'qty_delivered': 1,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
# create invoice lines and validate it
move_form = Form(self.AccountMove)
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_cost']
line_form.quantity = 3.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_cost']
line_form.quantity = 3.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_a = move_form.save()
invoice_a.action_post()
sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.company_data['product_order_cost'])
sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.company_data['product_delivery_cost'])
self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product")
self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product")
self.assertEqual(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)")
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)")
self.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, 3, 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, 3, 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")
# create second invoice lines and validate it
move_form = Form(self.AccountMove)
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_cost']
line_form.quantity = 2.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_cost']
line_form.quantity = 2.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_b = move_form.save()
invoice_b.action_post()
sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.company_data['product_order_cost'])
sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.company_data['product_delivery_cost'])
self.assertTrue(sale_order_line5, "A new sale line should have been created with ordered product")
self.assertTrue(sale_order_line6, "A new sale line should have been created with delivered product")
self.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, 2, 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, 2, 0), 'Sale line 6 is wrong after confirming 2e vendor invoice')
@freeze_time('2020-01-15')
def test_sales_team_invoiced(self):
""" Test invoiced field from sales team ony take into account the amount the sales channel has invoiced this month """
invoices = self.env['account.move'].create([
{
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2020-01-10',
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 1000.0})],
},
{
'move_type': 'out_refund',
'partner_id': self.partner_a.id,
'invoice_date': '2020-01-10',
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 500.0})],
},
{
'move_type': 'in_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2020-01-01',
'date': '2020-01-01',
'invoice_line_ids': [(0, 0, {'product_id': self.product_a.id, 'price_unit': 800.0})],
},
])
invoices.action_post()
for invoice in invoices:
self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=invoice.ids)\
.create({})\
._create_payments()
invoices.flush_model()
self.assertRecordValues(invoices.team_id, [{'invoiced': 500.0}])
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_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,
'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,
'qty_delivered': 1,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
# create invoice lines and validate it
move_form = Form(self.AccountMove)
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_sales_price']
line_form.quantity = 3.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_sales_price']
line_form.quantity = 3.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_a = move_form.save()
invoice_a.action_post()
sale_order_line3 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol.product_id == self.company_data['product_delivery_sales_price'])
sale_order_line4 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol.product_id == self.company_data['product_order_sales_price'])
self.assertTrue(sale_order_line3, "A new sale line should have been created with ordered product")
self.assertTrue(sale_order_line4, "A new sale line should have been created with delivered product")
self.assertEqual(len(self.sale_order.order_line), 4, "There should be 4 lines on the SO (2 vendor bill lines created)")
self.assertEqual(len(self.sale_order.order_line.filtered(lambda sol: sol.is_expense)), 2, "There should be 4 lines on the SO (2 vendor bill lines created)")
self.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, 3, 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, 3, 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")
# create second invoice lines and validate it
move_form = Form(self.AccountMove)
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_sales_price']
line_form.quantity = 2.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_sales_price']
line_form.quantity = 2.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_b = move_form.save()
invoice_b.action_post()
sale_order_line5 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line1 and sol != sale_order_line3 and sol.product_id == self.company_data['product_delivery_sales_price'])
sale_order_line6 = self.sale_order.order_line.filtered(lambda sol: sol != sale_order_line2 and sol != sale_order_line4 and sol.product_id == self.company_data['product_order_sales_price'])
self.assertFalse(sale_order_line5, "No new sale line should have been created with delivered product !!")
self.assertTrue(sale_order_line6, "A new sale line should have been created with ordered product")
self.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, 3, 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_distribution` to be visible in the view
self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')
# confirm SO
self.env['sale.order.line'].create({
'product_id': self.company_data['product_delivery_no'].id,
'product_uom_qty': 2,
'qty_delivered': 1,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
# create invoice lines and validate it
move_form = Form(self.AccountMove)
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_delivery_no']
line_form.quantity = 3.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice_a = move_form.save()
invoice_a.action_post()
self.assertEqual(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) when validating vendor bill")
self.assertTrue(invoice_a.mapped('line_ids.analytic_line_ids'), "Analytic lines should be generated")
def test_not_reinvoicing_invoiced_so_lines(self):
""" Test that invoiced SO lines are not re-invoiced. """
so_line1 = self.env['sale.order.line'].create({
'product_id': self.company_data['product_delivery_cost'].id,
'discount': 100.00,
'order_id': self.sale_order.id,
})
so_line2 = self.env['sale.order.line'].create({
'product_id': self.company_data['product_delivery_sales_price'].id,
'discount': 100.00,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
for line in self.sale_order.order_line:
line.qty_delivered = 1
# create invoice and validate it
invoice = self.sale_order._create_invoices()
invoice.action_post()
so_line3 = self.sale_order.order_line.filtered(lambda sol: sol != so_line1 and sol.product_id == self.company_data['product_delivery_cost'])
so_line4 = self.sale_order.order_line.filtered(lambda sol: sol != so_line2 and sol.product_id == self.company_data['product_delivery_sales_price'])
self.assertFalse(so_line3, "No re-invoicing should have created a new sale line with product #1")
self.assertFalse(so_line4, "No re-invoicing should have created a new sale line with product #2")
self.assertEqual(so_line1.qty_delivered, 1, "No re-invoicing should have impacted exising SO line 1")
self.assertEqual(so_line2.qty_delivered, 1, "No re-invoicing should have impacted exising SO line 2")
def test_not_recomputing_unit_price_for_expensed_so_lines(self):
# 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)
sol_1 = self.env['sale.order.line'].create({
'product_id': self.company_data['product_order_cost'].id,
'product_uom_qty': 2,
'qty_delivered': 1,
'order_id': self.sale_order.id,
})
self.sale_order.action_confirm()
# create invoice lines and validate it
move_form = Form(self.AccountMove)
move_form.partner_id = self.partner_a
with move_form.invoice_line_ids.new() as line_form:
line_form.product_id = self.company_data['product_order_cost']
line_form.quantity = 3.0
line_form.analytic_distribution = {self.analytic_account.id: 100}
invoice = move_form.save()
invoice.action_post()
# update the quantity of the expensed line
sol_2 = self.sale_order.order_line.filtered(lambda sol: sol != sol_1 and sol.product_id == self.company_data['product_order_cost'])
sol_2_subtotal_before = sol_2.price_unit
sol_2.product_uom_qty = 3.0
sol_2_subtotal_after = sol_2.price_unit
self.assertEqual(sol_2_subtotal_before, sol_2_subtotal_after)
def test_cost_invoicing(self):
""" Test confirming a vendor invoice to reinvoice cost on the so """
serv_cost = self.env['product.product'].create({
'name': "Ordered at cost",
'standard_price': 160,
'list_price': 180,
'type': 'consu',
'invoice_policy': 'order',
'expense_policy': 'cost',
'default_code': 'PROD_COST',
'service_type': 'manual',
})
prod_gap = self.company_data['product_service_order']
project = self.env['project.project'].create({'name': 'SO Project'})
if not project.account_id:
project._create_analytic_account()
self.sale_order.write({
'project_id': project.id,
'order_line': [Command.create({
'product_id': prod_gap.id,
'product_uom_qty': 2,
'price_unit': prod_gap.list_price,
})],
})
self.sale_order.action_confirm()
inv = self.env['account.move'].with_context(default_move_type='in_invoice').create({
'partner_id': self.partner_a.id,
'invoice_date': self.sale_order.date_order,
'invoice_line_ids': [
Command.create({
'name': serv_cost.name,
'product_id': serv_cost.id,
'product_uom_id': serv_cost.uom_id.id,
'quantity': 2,
'price_unit': serv_cost.standard_price,
'analytic_distribution': {self.sale_order.project_account_id.id: 100},
}),
],
})
inv.action_post()
sol = self.sale_order.order_line.filtered(lambda l: l.product_id == serv_cost)
self.assertTrue(sol, 'Sale: cost invoicing does not add lines when confirming vendor invoice')
self.assertEqual(
(sol.price_unit, sol.qty_delivered, sol.product_uom_qty, sol.qty_invoiced),
(160, 2, 2, 0),
'Sale: line is wrong after confirming vendor invoice')
def test_invoice_analytic_account_so_not_default(self):
""" Tests whether, when an analytic account rule is set and the so has an analytic account,
the default analytic account is not replaced by the one from the so in the invoice.
"""
analytic_plan_default = self.env['account.analytic.plan'].create({'name': 'default'})
analytic_account_default = self.env['account.analytic.account'].create({'name': 'default', 'plan_id': analytic_plan_default.id})
analytic_account_so = self.env['account.analytic.account'].create({'name': 'so', 'plan_id': analytic_plan_default.id})
self.env['account.analytic.distribution.model'].create({
'analytic_distribution': {analytic_account_default.id: 100},
'product_id': self.product_a.id,
})
project = self.env['project.project'].create({
'name': 'SO Project',
f'{analytic_plan_default._column_name()}': analytic_account_so.id,
})
# Remove the analytic account auto-generated when creating a timesheetable project if it exists
project.account_id = False
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.partner_a
so_form.project_id = project
with so_form.order_line.new() as sol:
sol.product_id = self.product_a
sol.product_uom_qty = 1
so = so_form.save()
so.action_confirm()
so._force_lines_to_invoice_policy_order()
so_context = {
'active_model': 'sale.order',
'active_ids': [so.id],
'active_id': so.id,
'default_journal_id': self.company_data['default_journal_sale'].id,
}
down_payment = self.env['sale.advance.payment.inv'].with_context(so_context).create({})
down_payment.create_invoices()
aml = self.env['account.move.line'].search([('move_id', 'in', so.invoice_ids.ids)])[0]
self.assertRecordValues(aml, [{'analytic_distribution': {str(analytic_account_default.id): 100}}])

View file

@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import tagged
from .common import TestSaleProjectCommon
@tagged('post_install', '-at_install')
class TestResConfigSettings(TestSaleProjectCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_b.id,
'partner_invoice_id': cls.partner_b.id,
'partner_shipping_id': cls.partner_b.id,
'order_line': [
Command.create({
'product_id': cls.product_milestone.id,
'product_uom_qty': 3,
}),
Command.create({
'product_id': cls.product_delivery_manual1.id,
'product_uom_qty': 2,
})
]
})
cls.product_milestone_sale_line = cls.sale_order.order_line.filtered(lambda sol: sol.product_id == cls.product_milestone)
cls.product_delivery_manual1_sale_line = cls.sale_order.order_line.filtered(lambda sol: sol.product_id == cls.product_delivery_manual1)
cls.sale_order.action_confirm()
cls.milestone = cls.env['project.milestone'].create({
'name': 'Test Milestone',
'sale_line_id': cls.product_milestone_sale_line.id,
'project_id': cls.project_global.id,
})
def test_disable_and_enable_project_milestone_feature(self):
self.assertTrue(self.env.user.has_group('project.group_project_milestone'), 'The Project Milestones feature should be enabled.')
self.set_project_milestone_feature(False)
self.assertFalse(self.env.user.has_group('project.group_project_milestone'), 'The Project Milestones feature should be disabled.')
product_milestones = self.product_milestone + self.product_milestone2
self.assertEqual(
product_milestones.mapped('service_policy'),
['delivered_manual'] * 2,
'Both milestone products should become a manual product when the project milestones feature is disabled')
self.assertEqual(
product_milestones.mapped('service_type'),
['manual'] * 2,
'Both milestone products should become a manual product when the project milestones feature is disabled')
self.assertEqual(
self.product_milestone_sale_line.qty_delivered_method,
'manual',
'The quantity delivered method of SOL containing milestone product should be changed to manual when the project milestones feature is disabled')
# Since the quantity delivered manual is manual then the user can manually change the quantity delivered
self.product_milestone_sale_line.qty_delivered = 2
# Enable the project milestones feature
self.set_project_milestone_feature(True)
self.assertEqual(
self.product_milestone.service_policy,
'delivered_milestones',
'The product has been updated and considered as milestones product since a SOL containing this product is linked to a milestone.')
self.assertEqual(
self.product_milestone.service_type,
'milestones',
'The product has been updated and considered as milestones product since a SOL containing this product is linked to a milestone.')
self.assertEqual(
self.product_milestone2.service_policy,
'delivered_manual',
'The product should not be updated since we cannot assume this product was a milestone when the feature'
' was enabled because no SOL with this product is linked to a milestone.')
self.assertEqual(
self.product_milestone2.service_type,
'manual',
'The product should not be updated since we cannot assume this product was a milestone when the feature'
' was enabled because no SOL with this product is linked to a milestone.')
self.assertEqual(
self.product_milestone_sale_line.qty_delivered_method,
'manual',
'The quantity delivered method of SOL containing milestone product should keep the same quantity delivered method even if the project milestones feature is renabled.')
self.assertEqual(self.product_milestone_sale_line.qty_delivered, 2, 'The quantity delivered should be the one set by the user.')

View file

@ -0,0 +1,90 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_project.tests.test_project_profitability import TestProjectProfitabilityCommon as Common
class TestProjectDashboardCommon(Common):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.dashboard_project = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
'name': 'Project',
'partner_id': cls.partner.id,
'account_id': cls.analytic_account.id,
'allow_billable': True,
})
cls.dashboard_sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': cls.partner.id,
'partner_invoice_id': cls.partner.id,
'partner_shipping_id': cls.partner.id,
})
cls.dashboard_sale_order.action_confirm()
cls.dashboardSaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True, default_order_id=cls.dashboard_sale_order.id)
cls.dashboard_product_delivery_service, cls.product_milestone, cls.product_prepaid = cls.env['product.product'].create([{
'name': "Service Delivery",
'standard_price': 30,
'list_price': 90,
'type': 'service',
'invoice_policy': 'delivery',
'service_type': 'manual',
'uom_id': cls.uom_hour.id,
'default_code': 'SERV-ORDERED2',
'service_tracking': 'task_global_project',
'project_id': cls.dashboard_project.id,
}, {
'name': "Service Milestone",
'standard_price': 30,
'list_price': 90,
'type': 'service',
'invoice_policy': 'delivery',
'service_type': 'milestones',
'uom_id': cls.uom_hour.id,
'default_code': 'SERV-ORDERED2',
'service_tracking': 'task_global_project',
'project_id': cls.dashboard_project.id,
}, {
'name': "Product prepaid",
'standard_price': 30,
'list_price': 90,
'type': 'service',
'uom_id': cls.uom_hour.id,
'default_code': 'SERV-ORDERED2',
'service_tracking': 'task_global_project',
'project_id': cls.dashboard_project.id,
}])
class TestDashboardProject(TestProjectDashboardCommon):
"""
This test ensures that the method get_sale_item_data compute correctly the data needed for the project_profitability sale sub section.
Since the data is different for the same input when the timesheet module is installed, those tests have to be run at_install
"""
def test_get_sale_item_data_various_sols(self):
"""This test ensures that the sols are computed and put into the correct profitability sections"""
hour_uom_id = self.env.ref('uom.product_uom_hour').id
unit_uom_id = self.env.ref('uom.product_uom_unit').id
sol_service_1, sol_service_2, sol_service_3, sol_service_4 = 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_service.id,
'product_uom_qty': 1,
}])
expected_dict = sol_service_3._read_format(
['display_name', 'product_uom_qty', 'qty_delivered', 'qty_invoiced', 'product_uom_id', 'product_id']
)
sale_item_data = self.dashboard_project.get_sale_items_data(limit=5, with_action=False, section_id='materials')
self.assertEqual(sale_item_data['sol_items'], expected_dict)
expected_dict = (sol_service_1 + sol_service_2 + sol_service_4)._read_format(
['display_name', 'product_uom_qty', 'qty_delivered', 'qty_invoiced', 'product_uom_id', 'product_id']
)
sale_item_data = self.dashboard_project.get_sale_items_data(limit=5, with_action=False, section_id='service_revenues')
self.assertEqual(sale_item_data['sol_items'], expected_dict)

View file

@ -0,0 +1,71 @@
from odoo.tests import TransactionCase
from odoo.tests import Form
class TestSaleOrderAccess(TransactionCase):
def setUp(self):
self.company_1 = self.env['res.company'].create({
'name': 'Company 1',
'currency_id': self.env.ref('base.USD').id,
})
self.company_2 = self.env['res.company'].create({
'name': 'Company 2',
'currency_id': self.env.ref('base.EUR').id,
})
self.user_company_1 = self.env['res.users'].create({
'name': 'User 1',
'login': 'user1',
'password': 'password',
'company_ids': [(6, 0, [self.company_1.id])],
'company_id': self.company_1.id,
'group_ids': [(6, 0, [
self.env.ref('sales_team.group_sale_manager').id,
self.env.ref('project.group_project_manager').id,
])]
})
self.admin_user = self.env['res.users'].create({
'name': 'Admin User',
'login': 'adminn',
'password': 'password',
'company_ids': [(6, 0, [self.company_1.id, self.company_2.id])],
'company_id': self.company_1.id,
'group_ids': [(6, 0, [
self.env.ref('sales_team.group_sale_manager').id,
self.env.ref('project.group_project_manager').id,
])],
})
self.partner = self.env['res.partner'].create({
'name': 'XYZ',
'type': 'contact'
})
self.project_company_2 = self.env['project.project'].create({
'name': 'Project Company 2',
'user_id': self.admin_user.id,
'company_id': self.company_2.id,
'partner_id': self.partner.id,
'allow_billable': True,
})
self.sale_order_company_1 = self.env['sale.order'].create({
'user_id': self.admin_user.id,
'partner_id': self.partner.id,
'company_id': self.company_1.id,
'state': 'sale',
'project_id': self.project_company_2.id
})
self.sale_line = self.env['sale.order.line'].create({
'name': 'XA',
'product_uom_qty': 1.00,
'price_unit': 20.00,
'order_id': self.sale_order_company_1.id,
'project_id': self.project_company_2.id,
})
self.project_company_2.write({
'sale_order_id': self.sale_order_company_1.id,
'sale_line_id': self.sale_line.id,
})
def test_user_with_company_1_access_can_open_sale_order(self):
Form(self.sale_order_company_1.with_user(self.admin_user).with_company(self.company_1))
Form(self.sale_order_company_1.with_user(self.user_company_1).with_company(self.company_1))

View file

@ -1,20 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import Form
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.exceptions import ValidationError
from odoo.tests.common import tagged
from psycopg2 import Error as Psycopg2Error
from psycopg2.errors import NotNullViolation
@tagged('post_install', '-at_install')
class TestSoLineMilestones(TestSaleCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
cls.env.user.group_ids += cls.quick_ref('project.group_project_manager')
cls.env['res.config.settings'].create({'group_project_milestone': True}).execute()
cls.env.user.group_ids += cls.env.ref('project.group_project_milestone')
uom_hour = cls.env.ref('uom.product_uom_hour')
cls.product_delivery_milestones1 = cls.env['product.product'].create({
@ -24,7 +27,6 @@ class TestSoLineMilestones(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': uom_hour.id,
'uom_po_id': uom_hour.id,
'default_code': 'MILE-DELI4',
'service_type': 'milestones',
'service_tracking': 'project_only',
@ -36,7 +38,6 @@ class TestSoLineMilestones(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': uom_hour.id,
'uom_po_id': uom_hour.id,
'default_code': 'MILE-DELI4',
'service_type': 'milestones',
'service_tracking': 'project_only',
@ -48,7 +49,6 @@ class TestSoLineMilestones(TestSaleCommon):
'type': 'service',
'invoice_policy': 'delivery',
'uom_id': uom_hour.id,
'uom_po_id': uom_hour.id,
'default_code': 'MILE-DELI4',
'service_type': 'milestones',
'service_tracking': 'task_in_project',
@ -137,6 +137,32 @@ class TestSoLineMilestones(TestSaleCommon):
self.project.sale_line_id = self.sol2
self.assertEqual(task.sale_line_id, self.sol1, 'The task should keep the SOL linked to the milestone.')
def test_default_values_milestone(self):
""" This test checks that newly created milestones have the correct default values:
1) the first SOL of the SO linked to the project should be used as the default one.
2) the quantity percentage should be 100% (1.0 in backend).
"""
project = self.env['project.project'].create({
'name': 'Test project',
'sale_line_id': self.sol2.id, # sol1 was created first so we use sol2 to demonstrate that sol1 is used
})
milestone = self.env['project.milestone'].with_context({'default_project_id': project.id}).create({
'name': 'Test milestone',
'project_id': project.id,
'is_reached': False,
})
# since SOL1 was created before SOL2, it should be selected
self.assertEqual(milestone.sale_line_id, self.sol1, "The milestone's sale order line should be the first one in the project's SO") #1
self.assertEqual(milestone.quantity_percentage, 1.0, "The milestone's quantity percentage should be 1.0") #2
def test_compute_qty_milestone(self):
""" This test will check that the compute methods for the milestone quantity fields work properly. """
ratio = self.milestone1.quantity_percentage / self.milestone1.product_uom_qty
self.milestone1.quantity_percentage = 1.0
self.assertEqual(self.milestone1.quantity_percentage / self.milestone1.product_uom_qty, ratio, "The ratio should be the same as before")
self.milestone1.product_uom_qty = 25
self.assertEqual(self.milestone1.quantity_percentage / self.milestone1.product_uom_qty, ratio, "The ratio should be the same as before")
def test_create_milestone_on_project_set_on_sales_order(self):
"""
Regression Test:
@ -145,15 +171,10 @@ class TestSoLineMilestones(TestSaleCommon):
the project for the milestone should be the one set on the SO,
and no ValidationError or NotNullViolation should be raised.
"""
project = self.env['project.project'].create({
'name': 'Test Project For Milestones',
'partner_id': self.partner_a.id
})
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,
'project_id': project.id, # the user set a project on the SO
})
self.env['sale.order.line'].create({
'product_id': self.product_delivery_milestones3.id,
@ -162,17 +183,138 @@ class TestSoLineMilestones(TestSaleCommon):
})
try:
sale_order.action_confirm()
except ValidationError:
except (ValidationError, NotNullViolation):
self.fail("The sale order should be confirmed, "
"and no ValidationError should be raised, "
"for a missing project on the milestone.")
except Psycopg2Error as e:
# Check if the error is a NOT NULL violation
NotNullViolationPgCode = '23502'
if e.pgcode == NotNullViolationPgCode:
self.fail("The sale order should be confirmed, "
"and no NotNullViolation should be raised, "
"for a missing project on the milestone.")
else:
# Re-raise any other unexpected database error
raise
"and no ValidationError or NotNullViolation should be raised, "
"for a missing project on the milestone.")
def test_so_with_milestone_products(self):
"""
If a SO contains products invoiced based on milestones, a milestone should be created for each of them
in their project.
"""
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
products = self.product_delivery_milestones1 | self.product_delivery_milestones2 | self.product_delivery_milestones3
products.service_tracking = 'task_in_project'
self.env['sale.order.line'].create([{
'product_id': product.id,
'product_uom_qty': 20,
'order_id': sale_order.id,
} for product in products])
sale_order.action_confirm()
project = sale_order.project_ids
self.assertEqual(len(project.milestone_ids), 3, "The project should have a milestone for each product.")
self.assertCountEqual({m.name for m in project.milestone_ids}, {f"[{products[0].default_code}] {p.name}" for p in products}, "The milestones should be named after the products.")
def test_project_template_with_milestones(self):
"""
If a milestone product has a project template with configured milestones, use those instead of creating
a new milestone and set a quantity equal to the quantity of the SOL divided by the number of milestones.
"""
project_template = self.env['project.project'].create({
'name': 'Project Template',
'allow_milestones': True,
})
self.env['project.milestone'].create([{
'project_id': project_template.id,
'name': str(i),
} for i in range(4)])
self.product_delivery_milestones1.project_template_id = project_template.id
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
self.env['sale.order.line'].create({
'product_id': self.product_delivery_milestones1.id,
'product_uom_qty': 20,
'order_id': sale_order.id,
})
sale_order.action_confirm()
project = sale_order.project_ids
self.assertEqual(len(project.milestone_ids), 4, "The generated project should have 4 milestones.")
self.assertEqual({m.quantity_percentage for m in project.milestone_ids}, {0.25}, "All milestones of the generated project should have a quantity percentage of 25%.")
self.assertTrue(project.allow_milestones, "The project should allow milestones as it was created from a product configured to create milestones.")
def test_project_template_with_milestones_multiple_products(self):
"""
If multiple products use the same project template, which has configured milestones, use the first product
on those milestones, but generate the other default milestones as normal
"""
project_template = self.env['project.project'].create({
'name': 'Project Template',
'allow_milestones': True,
})
self.env['project.milestone'].create([{
'project_id': project_template.id,
'name': str(i),
} for i in range(4)])
products = self.product_delivery_milestones1 | self.product_delivery_milestones2
products.write({
'project_template_id': project_template.id,
'service_tracking': 'task_in_project',
})
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
self.env['sale.order.line'].create([{
'product_id': product.id,
'product_uom_qty': 20,
'order_id': sale_order.id,
} for product in products])
sale_order.action_confirm()
project = sale_order.project_ids
self.assertEqual(len(project.milestone_ids), 5, "The project should have 5 milestones")
self.assertTrue(project.allow_milestones, "The project should allow milestones as it was created from a product configured to create milestones.")
def test_subtask_milestone_sol(self):
""" A task should keep its sale line according to its milestone is changed. """
# Create a sale order with two milestone lines
sale_order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_delivery_milestones3.id,
'product_uom_qty': 1,
'name': name,
}) for name in ["m1", "m2"]
]
})
sale_order.action_confirm()
# Case 1: parent task is present set SOL according parent's SOL
parent_task = self.env['project.task'].create({
'name': 'Test Task',
'partner_id': self.partner.id,
'project_id': sale_order.project_id.id,
'sale_line_id': self.sol1.id
})
tasks = sale_order.project_id.task_ids
tasks[0].parent_id = parent_task.id
with Form(tasks[0]) as task_form:
task_form.sale_line_id = self.env['sale.order.line']
task_form.milestone_id = tasks[1].milestone_id
self.assertEqual(tasks[0].sale_line_id,
parent_task.sale_line_id,
"Task should have the correct sale line based on parent task.")
# Case 2: parent task not present set SOL according Milestone's SOL
tasks[0].parent_id = False
with Form(tasks[0]) as task_form:
task_form.sale_line_id = self.env['sale.order.line']
task_form.milestone_id = tasks[0].milestone_id
self.assertEqual(tasks[0].sale_line_id,
tasks[0].milestone_id.sale_line_id,
"Task should have the correct sale line based on milestone.")
# Case 3: parent task and milestone not present set SOL according project's SOL
with Form(tasks[0]) as task_form:
task_form.sale_line_id = self.env['sale.order.line']
task_form.milestone_id = self.env['project.milestone']
self.assertEqual(tasks[0].sale_line_id,
tasks[0].project_id.sale_line_id,
"Task should have the correct sale line based on project.")