493 lines
21 KiB
Python
493 lines
21 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import lxml
|
|
|
|
from datetime import date
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import Command
|
|
from odoo.addons.project.tests.test_project_base import TestProjectCommon
|
|
from odoo.tests import tagged
|
|
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestProject(TestProjectCommon):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
|
'name': 'Plan',
|
|
})
|
|
|
|
cls.analytic_account = cls.env['account.analytic.account'].create({
|
|
'name': 'Project - AA',
|
|
'code': 'AA-1234',
|
|
'plan_id': cls.analytic_plan.id,
|
|
})
|
|
cls.project_goats.write({
|
|
'account_id': cls.analytic_account.id,
|
|
})
|
|
|
|
def test_get_budget_items_expense(self):
|
|
"""
|
|
Check budget items data with expense budget only
|
|
"""
|
|
if not self.project_pigs.account_id:
|
|
self.assertEqual(self.project_pigs._get_budget_items(False), None, 'No budget should be return since no AA is set into the project.')
|
|
self.assertTrue(self.env.user.has_group('analytic.group_analytic_accounting'))
|
|
self.assertDictEqual(
|
|
self.project_goats._get_budget_items(False),
|
|
{
|
|
'data': [],
|
|
'total': {'allocated': 0, 'progress': 0, 'spent': 0},
|
|
'can_add_budget': False,
|
|
},
|
|
'No budget has been created for this project.'
|
|
)
|
|
|
|
self.assertEqual(self.project_goats.account_id, self.analytic_account, 'The project and budget line will use the same analytic account on the same plan.')
|
|
plan_fname = self.analytic_account.plan_id._column_name()
|
|
today = date.today()
|
|
budget_1 = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Expense Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'expense',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': 500,
|
|
})],
|
|
})
|
|
budget_1.action_budget_confirm()
|
|
|
|
budget_items = self.project_goats._get_budget_items(False)
|
|
del budget_items['data'][0]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][0]['id']
|
|
self.assertDictEqual(budget_items, {
|
|
'data': [
|
|
{
|
|
'allocated': 500.0,
|
|
'budgets': [],
|
|
'budget_type': 'expense',
|
|
'progress': 1.0,
|
|
'spent': 0.0,
|
|
},
|
|
],
|
|
'total': {'allocated': -500.0, 'spent': 0.0, 'progress': 1.0},
|
|
'can_add_budget': False,
|
|
})
|
|
|
|
def test_get_budget_items_revenue(self):
|
|
"""
|
|
Check budget items data with revenue budget only
|
|
"""
|
|
self.assertEqual(self.project_goats.account_id, self.analytic_account, 'The project and budget line will use the same analytic account on the same plan.')
|
|
plan_fname = self.analytic_account.plan_id._column_name()
|
|
today = date.today()
|
|
budget_1 = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Revenue 1 Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'revenue',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': 500,
|
|
})],
|
|
})
|
|
budget_1.action_budget_confirm()
|
|
|
|
budget_items = self.project_goats._get_budget_items(False)
|
|
del budget_items['data'][0]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][0]['id']
|
|
self.assertDictEqual(budget_items, {
|
|
'data': [
|
|
{
|
|
'allocated': 500.0,
|
|
'budgets': [],
|
|
'budget_type': 'revenue',
|
|
'progress': -1.0,
|
|
'spent': 0.0,
|
|
},
|
|
],
|
|
'total': {'allocated': 500.0, 'spent': 0.0, 'progress': -1.0},
|
|
'can_add_budget': False,
|
|
})
|
|
|
|
def test_get_budget_items_both(self):
|
|
"""
|
|
Check budget items data with both budget only
|
|
"""
|
|
self.assertEqual(self.project_goats.account_id, self.analytic_account, 'The project and budget line will use the same analytic account on the same plan.')
|
|
plan_fname = self.analytic_account.plan_id._column_name()
|
|
today = date.today()
|
|
budget_1 = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Both Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'both',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': 500,
|
|
})],
|
|
})
|
|
budget_1.action_budget_confirm()
|
|
|
|
budget_items = self.project_goats._get_budget_items(False)
|
|
del budget_items['data'][0]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][0]['id']
|
|
self.assertDictEqual(budget_items, {
|
|
'data': [
|
|
{
|
|
'allocated': 500.0,
|
|
'budgets': [],
|
|
'budget_type': 'both',
|
|
'progress': -1.0,
|
|
'spent': 0.0,
|
|
},
|
|
],
|
|
'total': {'allocated': 500.0, 'spent': 0.0, 'progress': -1.0},
|
|
'can_add_budget': False,
|
|
})
|
|
|
|
def test_get_budget_items_expense_and_revenue(self):
|
|
"""
|
|
Check budget items data with expense and revenue budgets
|
|
"""
|
|
self.assertEqual(self.project_goats.account_id, self.analytic_account, 'The project and budget line will use the same analytic account on the same plan.')
|
|
plan_fname = self.analytic_account.plan_id._column_name()
|
|
today = date.today()
|
|
budget_1 = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Expense Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'expense',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': 500,
|
|
})],
|
|
})
|
|
budget_1.action_budget_confirm()
|
|
|
|
budget_2 = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Revenue Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'revenue',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': 1500,
|
|
})],
|
|
})
|
|
budget_2.action_budget_confirm()
|
|
|
|
budget_items = self.project_goats._get_budget_items(False)
|
|
del budget_items['data'][0]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][0]['id']
|
|
del budget_items['data'][1]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][1]['id']
|
|
self.assertDictEqual(budget_items, {
|
|
'data': [
|
|
{
|
|
'allocated': 500.0,
|
|
'budgets': [],
|
|
'budget_type': 'expense',
|
|
'progress': 1.0,
|
|
'spent': 0.0,
|
|
},
|
|
{
|
|
'allocated': 1500.0,
|
|
'budgets': [],
|
|
'budget_type': 'revenue',
|
|
'progress': -1.0,
|
|
'spent': 0.0,
|
|
},
|
|
],
|
|
'total': {'allocated': 1000.0, 'spent': 0.0, 'progress': -1.0},
|
|
'can_add_budget': False,
|
|
})
|
|
|
|
self.analytic_plan_pigs = self.env['account.analytic.plan'].create({
|
|
'name': 'Plan pigs',
|
|
})
|
|
|
|
self.analytic_account_pigs = self.env['account.analytic.account'].create({
|
|
'name': 'Project pigs - AA',
|
|
'code': 'AA-1234',
|
|
'plan_id': self.analytic_plan_pigs.id,
|
|
})
|
|
self.project_pigs.write({
|
|
'account_id': self.analytic_account_pigs.id,
|
|
})
|
|
|
|
self.assertEqual(self.project_pigs.account_id, self.analytic_account_pigs, 'The project and budget line will use the same analytic account on the same plan.')
|
|
plan_fname = self.analytic_account_pigs.plan_id._column_name()
|
|
budget_1 = self.env['budget.analytic'].create({
|
|
'name': 'Project Pigs Expense Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'expense',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account_pigs.id,
|
|
'budget_amount': 1500,
|
|
})],
|
|
})
|
|
budget_1.action_budget_confirm()
|
|
|
|
plan_fname = self.analytic_account_pigs.plan_id._column_name()
|
|
budget_2 = self.env['budget.analytic'].create({
|
|
'name': 'Project Pigs Revenue Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'revenue',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account_pigs.id,
|
|
'budget_amount': 500,
|
|
})],
|
|
})
|
|
budget_2.action_budget_confirm()
|
|
|
|
budget_items = self.project_pigs._get_budget_items(False)
|
|
del budget_items['data'][0]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][0]['id']
|
|
del budget_items['data'][1]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][1]['id']
|
|
self.assertDictEqual(budget_items, {
|
|
'data': [
|
|
{
|
|
'allocated': 1500.0,
|
|
'budgets': [],
|
|
'budget_type': 'expense',
|
|
'progress': 1.0,
|
|
'spent': 0.0,
|
|
},
|
|
{
|
|
'allocated': 500.0,
|
|
'budgets': [],
|
|
'budget_type': 'revenue',
|
|
'progress': -1.0,
|
|
'spent': 0.0,
|
|
},
|
|
],
|
|
'total': {'allocated': -1000.0, 'spent': 0.0, 'progress': 1.0},
|
|
'can_add_budget': False,
|
|
})
|
|
|
|
def test_get_budget_items_with_action(self):
|
|
self.assertEqual(self.project_goats.account_id, self.analytic_account, 'The project and budget line will use the same analytic account on the same plan.')
|
|
plan_fname = self.analytic_account.plan_id._column_name()
|
|
today = date.today()
|
|
budgets = self.env['budget.analytic']
|
|
for budget_name, planned_amount in [
|
|
('Project Goats Budget', 500),
|
|
('Project Pigs Budget', 1000),
|
|
]:
|
|
budget = self.env['budget.analytic'].create({
|
|
'name': budget_name,
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_line_ids': [
|
|
Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': planned_amount,
|
|
}),
|
|
],
|
|
})
|
|
budget.action_budget_confirm()
|
|
budgets += budget
|
|
|
|
self.env.user.group_ids += self.env.ref('account.group_account_user')
|
|
self.assertTrue(self.env.user.has_group('analytic.group_analytic_accounting'))
|
|
budget_items = self.project_goats.with_context({'allowed_company_ids': [self.env.company.id]})._get_budget_items(with_action=True)
|
|
del budget_items['data'][0]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][0]['id']
|
|
del budget_items['data'][1]['name'] # remove the name because it is a lazy translation.
|
|
del budget_items['data'][1]['id']
|
|
self.assertDictEqual(budget_items, {
|
|
'data': [
|
|
{
|
|
'allocated': 500.0,
|
|
'progress': 1.0,
|
|
'spent': 0.0,
|
|
'budgets': [],
|
|
'budget_type': 'expense',
|
|
'action': {
|
|
'name': 'action_view_budget_lines',
|
|
'type': 'object',
|
|
'args': f'[[["id", "in", {budgets[0].budget_line_ids.ids}]]]',
|
|
}
|
|
},
|
|
{
|
|
'allocated': 1000.0,
|
|
'progress': 1.0,
|
|
'spent': 0.0,
|
|
'budgets': [],
|
|
'budget_type': 'expense',
|
|
'action': {
|
|
'name': 'action_view_budget_lines',
|
|
'type': 'object',
|
|
'args': f'[[["id", "in", {budgets[1].budget_line_ids.ids}]]]',
|
|
}
|
|
},
|
|
],
|
|
'total': {'allocated': -1500.0, 'spent': 0.0, 'progress': 1.0},
|
|
'form_view_id': self.env.ref('project_account_budget.view_budget_analytic_form_dialog').id,
|
|
'can_add_budget': True,
|
|
'company_id': self.env.company.id,
|
|
})
|
|
|
|
def test_get_budget_items_plans_mismatch(self):
|
|
self.assertTrue(self.env.user.has_group('analytic.group_analytic_accounting'))
|
|
self.assertEqual(self.project_goats.account_id, self.analytic_account, 'The project and budget line will use the same analytic account.')
|
|
analytic_plan_2 = self.env['account.analytic.plan'].create({
|
|
'name': 'Plan 2',
|
|
})
|
|
self.assertNotEqual(self.project_goats.account_id.plan_id, analytic_plan_2, 'The project account and budget line will use different analytic plans.')
|
|
plan_fname = analytic_plan_2._column_name()
|
|
today = date.today()
|
|
budget = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: self.analytic_account.id,
|
|
'budget_amount': 500,
|
|
})],
|
|
})
|
|
budget.action_budget_confirm()
|
|
self.assertDictEqual(
|
|
self.project_goats._get_budget_items(False),
|
|
{
|
|
'data': [],
|
|
'total': {'allocated': 0, 'progress': 0, 'spent': 0},
|
|
'can_add_budget': False,
|
|
},
|
|
'No budget has been created for this project. Because the plan of the budget line does not match the plan of the account of the project.'
|
|
)
|
|
|
|
def test_project_update_template_value_with_project_user(self):
|
|
self.user_projectuser.group_ids |= self.env.ref('account.group_account_readonly')
|
|
self.project_goats.total_budget_amount = 1000
|
|
|
|
template_values = self.env['project.update'].with_user(self.user_projectuser)._get_template_values(
|
|
self.project_goats.with_user(self.user_projectuser)
|
|
)
|
|
|
|
self.assertEqual(template_values['profitability'], {})
|
|
self.assertFalse(template_values['show_profitability'])
|
|
|
|
def test_project_update_with_budget_and_vendor_bill(self):
|
|
"""Check project update calculations after expense budget and vendor bill.
|
|
|
|
Verifies that budget percentages and amounts are correctly displayed in the
|
|
project update description after creating a vendor bill.
|
|
"""
|
|
today = date.today()
|
|
# Use the project plan (where _column_name() returns 'account_id') so that
|
|
# the plan column and the account_id groupby in _compute_budget are consistent,
|
|
# and the budget report SQL JOIN matches analytic lines correctly.
|
|
project_plan, _other_plans = self.env['account.analytic.plan']._get_all_plans()
|
|
project_analytic_account = self.env['account.analytic.account'].create({
|
|
'name': 'Project Budget - AA',
|
|
'code': 'AA-BUDGET',
|
|
'plan_id': project_plan.id,
|
|
})
|
|
self.project_goats.write({'account_id': project_analytic_account.id})
|
|
plan_fname = project_analytic_account.plan_id._column_name()
|
|
expense_account = self.env['account.account'].create({
|
|
'name': 'Test Expense Account',
|
|
'code': 'TESTEXP',
|
|
'account_type': 'expense',
|
|
})
|
|
budget = self.env['budget.analytic'].create({
|
|
'name': 'Project Goats Expense Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'expense',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: project_analytic_account.id,
|
|
'budget_amount': 10000,
|
|
})],
|
|
})
|
|
budget.action_budget_confirm()
|
|
self.assertEqual(self.project_goats.account_id, project_analytic_account)
|
|
self.assertEqual(self.project_goats.total_budget_amount, 10000.0)
|
|
|
|
vendor_bill = self.env['account.move'].create({
|
|
'move_type': 'in_invoice',
|
|
'partner_id': self.partner_1.id,
|
|
'invoice_date': today,
|
|
'invoice_line_ids': [Command.create({
|
|
'name': 'Test Expense',
|
|
'account_id': expense_account.id,
|
|
'price_unit': 1000.0,
|
|
'analytic_distribution': {project_analytic_account.id: 100},
|
|
})],
|
|
})
|
|
vendor_bill.action_post()
|
|
# Invalidate cached achieved_amount so it is recomputed from budget.report
|
|
budget.budget_line_ids.invalidate_recordset(['achieved_amount'])
|
|
update = self.env['project.update'].with_context({'default_project_id': self.project_goats.id}).create({
|
|
'name': 'Test',
|
|
})
|
|
description_body = lxml.html.fromstring(update.description)
|
|
description_string = lxml.html.tostring(description_body.xpath('//div[@name="budget"]')[0]).decode('utf-8')
|
|
self.assertEqual(update.project_id, self.project_goats)
|
|
self.assertNotIn('-10.0%', description_string)
|
|
self.assertIn('1,000', description_string)
|
|
self.assertIn('10,000', description_string)
|
|
self.assertIn('90.0%', description_string)
|
|
self.assertIn('9,000', description_string)
|
|
self.assertNotIn('11,000', description_string)
|
|
|
|
def test_compute_budget_excludes_revised(self):
|
|
"""Verify total_budget_amount reflects only the confirmed budget when a revision exists."""
|
|
# _compute_budget groups by 'account_id' which maps to the project analytic plan column.
|
|
# Using a custom plan would map to a dynamic column (x_plan_N_id), causing 0 results.
|
|
project_plan, _other_plans = self.env['account.analytic.plan']._get_all_plans()
|
|
project_analytic_account = self.env['account.analytic.account'].create({
|
|
'name': 'Project Budget Revised - AA',
|
|
'code': 'AA-REVISED',
|
|
'plan_id': project_plan.id,
|
|
})
|
|
self.project_goats.write({'account_id': project_analytic_account.id})
|
|
plan_fname = project_analytic_account.plan_id._column_name()
|
|
today = date.today()
|
|
budget = self.env['budget.analytic'].create({
|
|
'name': 'Initial Budget',
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'expense',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: project_analytic_account.id,
|
|
'budget_amount': 10000,
|
|
})],
|
|
})
|
|
budget.action_budget_confirm()
|
|
self.assertEqual(self.project_goats.total_budget_amount, 10000.0)
|
|
|
|
# Create a revision with a higher amount and confirm it.
|
|
# Confirming the revision transitions the original budget to 'revised'.
|
|
revised_budget = self.env['budget.analytic'].create({
|
|
'name': 'Revised Budget',
|
|
'parent_id': budget.id,
|
|
'date_from': today.replace(day=1),
|
|
'date_to': today + relativedelta(months=1, days=-1),
|
|
'budget_type': 'expense',
|
|
'budget_line_ids': [Command.create({
|
|
plan_fname: project_analytic_account.id,
|
|
'budget_amount': 15000,
|
|
})],
|
|
})
|
|
revised_budget.action_budget_confirm()
|
|
|
|
self.assertEqual(budget.state, 'revised')
|
|
self.assertEqual(revised_budget.state, 'confirmed')
|
|
# Invalidate cache: total_budget_amount has no @api.depends, so Odoo won't
|
|
# automatically recompute after budget lines change state.
|
|
self.project_goats.invalidate_recordset(['total_budget_amount'])
|
|
self.assertEqual(
|
|
self.project_goats.total_budget_amount, 15000.0,
|
|
'total_budget_amount must only count the confirmed revision, not both the revised and confirmed budgets combined',
|
|
)
|