Files
odoo_source/addons/project_purchase/tests/test_project_profitability.py
T
Victor Piryns (pivi) 9ffe67a975 [FIX] project_purchase: consider analytic_distribution in costs
Current behaviour:
Project Update doesn't take into consideration the `analytic_distribution`
percentage of the analytic_account on the line of the purchase orders.

Expected behaviour:
Account correctly for the % of the analytic account for the project,
just like in the Accounting app.

Steps to reproduce:
- Install Accounting, Sales, Purchase, Timesheet, Inventory, Project
- Activate "Analytic accounts" in Settings
- Create a product service which is creating a project at sales
- Create a new SO, add the service, confirm the SO and invoice
- Create a new PO, add some random product, on that same line add 2
analytic account, 50% of the SO project we just created, and 50% from any other.
- Confirm the PO
- Go to the SO's project update section, observe the cost is 100% of the line cost, not 50%

Reason for the problem:
`analytic_distribution` ratios are not taken into account when calculation the costs
of the project.

Fix:
Get the ratio of the `analytic_distribution` and multiply by it when processing the costs

Affected versions:
- 16.0
- master

opw-3079537

closes odoo/odoo#108222

X-original-commit: b676222c129863c154efb48e454e144b77157617
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
2022-12-19 11:33:49 +01:00

255 lines
12 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo import Command
from odoo.tests import tagged
from odoo.addons.project.tests.test_project_profitability import TestProjectProfitabilityCommon
from odoo.addons.purchase.tests.test_purchase_invoice import TestPurchaseToInvoiceCommon
@tagged('-at_install', 'post_install')
class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurchaseToInvoiceCommon):
def test_bills_without_purchase_order_are_accounted_in_profitability(self):
"""
A bill that has an AAL on one of its line should be taken into account
for the profitability of the project.
"""
# create a bill_1 with the AAL
bill_1 = self.env['account.move'].create({
"name": "Bill_1 name",
"move_type": "in_invoice",
"state": "draft",
"partner_id": self.partner.id,
"invoice_date": datetime.today(),
"invoice_line_ids": [Command.create({
"analytic_distribution": {self.analytic_account.id: 100},
"product_id": self.product_a.id,
"quantity": 1,
"product_uom_id": self.product_a.uom_id.id,
"price_unit": self.product_a.standard_price,
})],
})
# the bill_1 is in draft, therefor it should have the cost "to_bill" same as the -product_price (untaxed)
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'other_purchase_costs',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
'to_bill': -self.product_a.standard_price,
'billed': 0.0,
}],
'total': {'to_bill': -self.product_a.standard_price, 'billed': 0.0},
},
)
# post bill_1
bill_1.action_post()
# we posted the bill_1, therefore the cost "billed" should be -product_price, to_bill should be back to 0
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'other_purchase_costs',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
'to_bill': 0.0,
'billed': -self.product_a.standard_price,
}],
'total': {'to_bill': 0.0, 'billed': -self.product_a.standard_price},
},
)
# create another bill, with 2 lines, 2 diff products, the second line has 2 as quantity
bill_2 = self.env['account.move'].create({
"name": "I have 2 lines",
"move_type": "in_invoice",
"state": "draft",
"partner_id": self.partner.id,
"invoice_date": datetime.today(),
"invoice_line_ids": [Command.create({
"analytic_distribution": {self.analytic_account.id: 100},
"product_id": self.product_a.id,
"quantity": 1,
"product_uom_id": self.product_a.uom_id.id,
"price_unit": self.product_a.standard_price,
}), Command.create({
"analytic_distribution": {self.analytic_account.id: 100},
"product_id": self.product_b.id,
"quantity": 2,
"product_uom_id": self.product_b.uom_id.id,
"price_unit": self.product_b.standard_price,
})],
})
# bill_2 is not posted, therefor its cost should be "to_billed" = - sum of all product_price * qty for each line
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'other_purchase_costs',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price),
'billed': -self.product_a.standard_price,
}],
'total': {
'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price),
'billed': -self.product_a.standard_price,
},
},
)
# post bill_2
bill_2.action_post()
# bill_2 is posted, therefor its cost should be counting in "billed", with the cost of bill_1
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'other_purchase_costs',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
'to_bill': 0.0,
'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price),
}],
'total': {
'to_bill': 0.0,
'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price),
},
},
)
# create a new purchase order
purchase_order = self.env['purchase.order'].create({
"name": "A purchase order",
"partner_id": self.partner_a.id,
"order_line": [Command.create({
"analytic_distribution": {self.analytic_account.id: 100},
"product_id": self.product_order.id,
"product_qty": 1,
"price_unit": self.product_order.standard_price,
})],
})
purchase_order.button_confirm()
# we should have a new section "purchase_order", the total should be updated,
# but the "other_purchase_costs" shouldn't change, as we don't takes into
# account bills from purchase orders, as those are already taken into calculations
# from the purchase orders (in "purchase_order" section)
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'purchase_order',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
'to_bill': -self.product_order.standard_price,
'billed': 0.0,
}, {
'id': 'other_purchase_costs',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
'to_bill': 0.0,
'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price),
}],
'total': {
'to_bill': -self.product_order.standard_price,
'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price),
},
},
)
purchase_order.action_create_invoice()
purchase_bill = purchase_order.invoice_ids # get the bill from the purchase
purchase_bill.invoice_date = datetime.today()
purchase_bill.action_post()
# now the bill has been posted, its costs should be accounted in the "billed" part
# of the purchase_order section, but should touch in the other_purchase_costs
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'purchase_order',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
'to_bill': 0.0,
'billed': -self.product_order.standard_price,
}, {
'id': 'other_purchase_costs',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
'to_bill': 0.0,
'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price),
}],
'total': {
'to_bill': 0.0,
'billed': -(2 * self.product_a.standard_price +
2 * self.product_b.standard_price +
self.product_order.standard_price),
},
},
)
def test_account_analytic_distribution_ratio(self):
"""
When adding multiple account analytics on a purchase line, and one of those
is from a project (for ex: project created on confirmed SO),
then in the profitability only the corresponding ratio of the analytic distribution
for that project analytic account should be taken into account.
(for ex: if there are 2 accounts on 1 line, one is 60% project analytic account, 40% some other,
then the profitability should only reflect 60% of the cost of the line, not 100%)
"""
# define the ratios for the analytic account of the line
analytic_ratios = {
"project_ratio": 60,
"other_ratio": 40,
}
self.assertEqual(sum(ratio for ratio in analytic_ratios.values()), 100)
# create another analytic_account that is not really relevant
other_analytic_account = self.env['account.analytic.account'].create({
'name': 'Not important',
'code': 'KO-1234',
'plan_id': self.analytic_plan.id,
})
# create a new purchase order
purchase_order = self.env['purchase.order'].create({
"name": "A purchase order",
"partner_id": self.partner_a.id,
"order_line": [Command.create({
"analytic_distribution": {
# this is the analytic_account that is linked to the project
self.analytic_account.id: analytic_ratios["project_ratio"],
other_analytic_account.id: analytic_ratios["other_ratio"],
},
"product_id": self.product_order.id,
"product_qty": 1,
"price_unit": self.product_order.standard_price,
})],
})
purchase_order.button_confirm()
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'purchase_order',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
'to_bill': -(self.product_order.standard_price * (analytic_ratios["project_ratio"] / 100)),
'billed': 0.0,
}],
'total': {
'to_bill': -(self.product_order.standard_price * (analytic_ratios["project_ratio"] / 100)),
'billed': 0.0,
},
},
)
purchase_order.action_create_invoice()
purchase_bill = purchase_order.invoice_ids # get the bill from the purchase
purchase_bill.invoice_date = datetime.today()
purchase_bill.action_post()
self.assertDictEqual(
self.project._get_profitability_items(False)['costs'],
{
'data': [{
'id': 'purchase_order',
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
'to_bill': 0.0,
'billed': -(self.product_order.standard_price * (analytic_ratios["project_ratio"] / 100)),
}],
'total': {
'to_bill': 0.0,
'billed': -(self.product_order.standard_price * (analytic_ratios["project_ratio"] / 100)),
},
},
)