mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-25 11:52:05 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
@ -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}}])
|
||||
|
|
@ -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.')
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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.")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue