mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-21 23:22:07 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -1,20 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import float_round, float_compare
|
||||
from odoo.tools import float_round
|
||||
|
||||
from odoo.addons.project.tests.test_project_profitability import TestProjectProfitabilityCommon
|
||||
from odoo.addons.purchase.tests.test_purchase_invoice import TestPurchaseToInvoiceCommon
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurchaseToInvoiceCommon):
|
||||
class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurchaseToInvoiceCommon, AccountTestInvoicingCommon):
|
||||
|
||||
def test_bills_without_purchase_order_are_accounted_in_profitability(self):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.group_ids |= cls.env.ref('purchase.group_purchase_user')
|
||||
cls.company_data_2 = cls.setup_other_company()
|
||||
|
||||
def _create_invoice_for_po(self, purchase_order):
|
||||
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()
|
||||
return purchase_bill
|
||||
|
||||
def test_bills_without_purchase_order_are_accounted_in_profitability_project_purchase(self):
|
||||
"""
|
||||
A bill that has an AAL on one of its line should be taken into account
|
||||
for the profitability of the project.
|
||||
|
|
@ -41,17 +55,32 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
"currency_id": self.env.company.currency_id.id,
|
||||
})],
|
||||
})
|
||||
# the bill_1 is in draft, therefor it should have the cost "to_bill" same as the -product_price (untaxed)
|
||||
# add 2 new AAL to the analytic account. Those costs must be present in the cost data
|
||||
self.env['account.analytic.line'].create([{
|
||||
'name': 'extra costs 1',
|
||||
'account_id': self.analytic_account.id,
|
||||
'amount': -50.1,
|
||||
}, {
|
||||
'name': 'extra costs 2',
|
||||
'account_id': self.analytic_account.id,
|
||||
'amount': -100,
|
||||
}])
|
||||
# the bill_1 is in draft, therefore 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_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
}, {
|
||||
'id': 'other_purchase_costs',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
|
||||
'to_bill': -self.product_a.standard_price * analytic_contribution,
|
||||
'billed': 0.0,
|
||||
}],
|
||||
'total': {'to_bill': -self.product_a.standard_price * analytic_contribution, 'billed': 0.0},
|
||||
'total': {'to_bill': -self.product_a.standard_price * analytic_contribution, 'billed': -150.1},
|
||||
},
|
||||
)
|
||||
# post bill_1
|
||||
|
|
@ -61,12 +90,17 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
self.project._get_profitability_items(False)['costs'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
}, {
|
||||
'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 * analytic_contribution,
|
||||
}],
|
||||
'total': {'to_bill': 0.0, 'billed': -self.product_a.standard_price * analytic_contribution},
|
||||
'total': {'to_bill': 0.0, 'billed': -self.product_a.standard_price * analytic_contribution - 150.1},
|
||||
},
|
||||
)
|
||||
# create another bill, with 3 lines, 2 diff products, the second line has 2 as quantity, the third line has a negative price
|
||||
|
|
@ -99,11 +133,16 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
"currency_id": self.env.company.currency_id.id,
|
||||
})],
|
||||
})
|
||||
# bill_2 is not posted, therefor its cost should be "to_billed" = - sum of all product_price * qty for each line
|
||||
# bill_2 is not posted, therefore 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_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
}, {
|
||||
'id': 'other_purchase_costs',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
|
||||
'to_bill': -(self.product_a.standard_price +
|
||||
|
|
@ -115,17 +154,22 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
'to_bill': -(self.product_a.standard_price +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price) * analytic_contribution,
|
||||
'billed': -self.product_a.standard_price * analytic_contribution,
|
||||
'billed': -self.product_a.standard_price * analytic_contribution - 150.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
# 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
|
||||
# bill_2 is posted, therefore 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_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
}, {
|
||||
'id': 'other_purchase_costs',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
|
||||
'to_bill': 0.0,
|
||||
|
|
@ -137,7 +181,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
'to_bill': 0.0,
|
||||
'billed': -(2 * self.product_a.standard_price +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price) * analytic_contribution,
|
||||
self.service_deliver.standard_price) * analytic_contribution - 150.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -154,15 +198,20 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
})],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
purchase_order.order_line.flush_recordset()
|
||||
# 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
|
||||
self.assertEqual(purchase_order.invoice_status, 'to invoice')
|
||||
# The section "purchase_order" should appear as the purchase order is validated, the total should be updated,
|
||||
# the "other_purchase_costs" shouldn't change, as we don't take 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': 'other_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
},{
|
||||
'id': 'purchase_order',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
|
||||
'to_bill': -self.product_order.standard_price * analytic_contribution,
|
||||
|
|
@ -171,28 +220,68 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
'id': 'other_purchase_costs',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'],
|
||||
'to_bill': 0.0,
|
||||
'billed': float_round(-(2 * self.product_a.standard_price +
|
||||
'billed': -(2 * self.product_a.standard_price +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price) * analytic_contribution, precision_digits=price_precision),
|
||||
self.service_deliver.standard_price) * analytic_contribution,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': -self.product_order.standard_price * analytic_contribution,
|
||||
'billed': float_round(-(2 * self.product_a.standard_price +
|
||||
'billed': -(2 * self.product_a.standard_price +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price) * analytic_contribution, precision_digits=price_precision),
|
||||
self.service_deliver.standard_price) * analytic_contribution - 150.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
# Create a vendor bill linked to the PO
|
||||
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
|
||||
self.assertEqual(purchase_order.invoice_ids.state, 'draft')
|
||||
# now the bill has been created and set to draft so the section "purchase_order" should appear, its costs should be accounted in the "to bill" 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': 'other_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
}, {
|
||||
'id': 'purchase_order',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
|
||||
'to_bill': -self.product_order.standard_price * analytic_contribution,
|
||||
'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 +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price) * analytic_contribution,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': -self.product_order.standard_price * analytic_contribution,
|
||||
'billed': -(2 * self.product_a.standard_price +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price) * analytic_contribution -150.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
# Post the vendor bill linked to the PO
|
||||
purchase_bill = purchase_order.invoice_ids
|
||||
purchase_bill.invoice_date = datetime.today()
|
||||
purchase_bill.action_post()
|
||||
self.assertEqual(purchase_order.invoice_ids.state, 'posted')
|
||||
# now the bill has been posted so the costs of the section "purchase_order" should be accounted in the "billed" part
|
||||
# and the total should be updated accordingly
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['costs'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_costs_aal',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_costs_aal'],
|
||||
'to_bill': 0.0,
|
||||
'billed': -150.1,
|
||||
}, {
|
||||
'id': 'purchase_order',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'],
|
||||
'to_bill': 0.0,
|
||||
|
|
@ -207,10 +296,10 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
}],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': float_round(-(2 * self.product_a.standard_price +
|
||||
'billed': -(2 * self.product_a.standard_price +
|
||||
2 * self.product_b.standard_price -
|
||||
self.service_deliver.standard_price +
|
||||
self.product_order.standard_price) * analytic_contribution, precision_digits=price_precision),
|
||||
self.product_order.standard_price) * analytic_contribution - 150.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -253,7 +342,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
})],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
purchase_order.order_line.flush_recordset()
|
||||
self.assertEqual(purchase_order.invoice_status, 'to invoice')
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['costs'],
|
||||
{
|
||||
|
|
@ -268,11 +357,12 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
'billed': 0.0,
|
||||
},
|
||||
},
|
||||
'No data should be found since the purchase order is not invoiced.',
|
||||
)
|
||||
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()
|
||||
|
||||
# Invoice the purchase order
|
||||
self._create_invoice_for_po(purchase_order)
|
||||
self.assertEqual(purchase_order.invoice_status, 'invoiced')
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['costs'],
|
||||
{
|
||||
|
|
@ -289,6 +379,237 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
},
|
||||
)
|
||||
|
||||
def test_multi_currency_for_project_purchase_profitability(self):
|
||||
""" This test ensures that when purchase orders with different currencies are linked to the same project, the amount are correctly computed according to the
|
||||
rate of the company """
|
||||
project = self.env['project.project'].create({'name': 'new project'})
|
||||
project._create_analytic_account()
|
||||
account = project.account_id
|
||||
foreign_company = self.company_data_2['company']
|
||||
foreign_company.currency_id = self.foreign_currency
|
||||
|
||||
# a custom analytic contribution (number between 1 -> 100 included)
|
||||
analytic_distribution = 42
|
||||
analytic_contribution = analytic_distribution / 100.
|
||||
# Create a bill_1 with the foreign_currency.
|
||||
bill_1 = self.env['account.move'].create({
|
||||
"name": "Bill foreign currency",
|
||||
"move_type": "in_invoice",
|
||||
"state": "draft",
|
||||
"partner_id": self.partner.id,
|
||||
"invoice_date": datetime.today(),
|
||||
"date": datetime.today(),
|
||||
"invoice_date_due": datetime.today() - timedelta(days=1),
|
||||
"company_id": foreign_company.id,
|
||||
"invoice_line_ids": [Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 1,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": self.product_a.standard_price,
|
||||
"currency_id": self.foreign_currency.id,
|
||||
}), Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 2,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": self.product_a.standard_price,
|
||||
"currency_id": self.foreign_currency.id,
|
||||
})],
|
||||
})
|
||||
# Ensures that if no items have the main currency, the total is still displayed in the main currency.
|
||||
# Expected total : product_price * 0.2 (rate) * 3 (number of products).
|
||||
# Note : for some reason, the method to round the amount to the rounding of the currency is not 100% reliable.
|
||||
# We use a float_compare in order to ensure the value is close enough to the expected result. This problem has no repercusion on the client side, since
|
||||
# there is also a rounding method on this side to ensure the amount is correctly displayed.
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('other_purchase_costs', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][0]['sequence'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 0.6, items['data'][0]['to_bill'], 2), 0)
|
||||
self.assertEqual(0.0, items['data'][0]['billed'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 0.6, items['total']['to_bill'], 2), 0)
|
||||
self.assertEqual(0.0, items['total']['billed'])
|
||||
|
||||
# Create a bill 2 with the main currency.
|
||||
bill_2 = self.env['account.move'].create({
|
||||
"name": "Bill main currency",
|
||||
"move_type": "in_invoice",
|
||||
"state": "draft",
|
||||
"partner_id": self.partner.id,
|
||||
"invoice_date": datetime.today(),
|
||||
"invoice_line_ids": [Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 1,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": self.product_a.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
}), Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 2,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": self.product_a.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
})],
|
||||
})
|
||||
|
||||
# The 2 bills are in draft, therefore the "to_bill" section should contain the total cost of the 2 bills.
|
||||
# The expected total is therefore product_price * 1 * 3 + product_price * 0.2 * 3 => * 3.6
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('other_purchase_costs', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][0]['sequence'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['data'][0]['to_bill'], 2), 0)
|
||||
self.assertEqual(0.0, items['data'][0]['billed'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['total']['to_bill'], 2), 0)
|
||||
self.assertEqual(0.0, items['total']['billed'])
|
||||
|
||||
# Bill 2 is posted. Its total is now in the 'billed' section, while the bill_1 is still in the 'to bill' one.
|
||||
bill_2.action_post()
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('other_purchase_costs', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][0]['sequence'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 0.6, items['data'][0]['to_bill'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3, items['data'][0]['billed'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 0.6, items['total']['to_bill'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3, items['total']['billed'], 2), 0)
|
||||
|
||||
# Bill 1 is posted. Its total is now in the 'billed' section, the 'to bill' one should now be empty.
|
||||
bill_1.action_post()
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('other_purchase_costs', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][0]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][0]['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['data'][0]['billed'], 2), 0)
|
||||
self.assertEqual(0.0, items['total']['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['total']['billed'], 2), 0)
|
||||
|
||||
# create a new purchase order with the foreign company
|
||||
purchase_order_foreign = self.env['purchase.order'].create({
|
||||
"name": "A foreign purchase order",
|
||||
"partner_id": self.partner_a.id,
|
||||
"company_id": foreign_company.id,
|
||||
"order_line": [Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 1,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.foreign_currency.id,
|
||||
}), Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 2,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.foreign_currency.id,
|
||||
})],
|
||||
})
|
||||
purchase_order_foreign.button_confirm()
|
||||
self.assertEqual(purchase_order_foreign.invoice_status, 'to invoice')
|
||||
|
||||
# The section "purchase_order" should appear because the purchase order is validated, the total should be updated,
|
||||
# but the "other_purchase_costs" shouldn't change, as we don't take into
|
||||
# account bills from purchase orders in this section.
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('purchase_order', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['purchase_order'], items['data'][0]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][0]['billed'])
|
||||
self.assertEqual(float_compare(-self.product_order.standard_price * analytic_contribution * 0.6, items['data'][0]['to_bill'], 2), 0)
|
||||
self.assertEqual('other_purchase_costs', items['data'][1]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][1]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][1]['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['data'][1]['billed'], 2), 0)
|
||||
self.assertEqual(float_compare(- self.product_order.standard_price * analytic_contribution * 0.6, items['total']['to_bill'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['total']['billed'], 2), 0)
|
||||
|
||||
# create a new purchase order
|
||||
purchase_order = self.env['purchase.order'].create({
|
||||
"name": "A foreign purchase order",
|
||||
"partner_id": self.partner_a.id,
|
||||
"company_id": self.env.company.id,
|
||||
"order_line": [Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 1,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
}), Command.create({
|
||||
"analytic_distribution": {account.id: analytic_distribution},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 2,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
})],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
self.assertEqual(purchase_order.invoice_status, 'to invoice')
|
||||
|
||||
# The section "purchase_order" should be updated with the new po values.
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('purchase_order', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['purchase_order'], items['data'][0]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][0]['billed'])
|
||||
self.assertEqual(float_compare(-self.product_order.standard_price * analytic_contribution * 3.6, items['data'][0]['to_bill'], 2), 0)
|
||||
self.assertEqual('other_purchase_costs', items['data'][1]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][1]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][1]['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['data'][1]['billed'], 2), 0)
|
||||
self.assertEqual(float_compare(- self.product_order.standard_price * analytic_contribution * 3.6 , items['total']['to_bill'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['total']['billed'], 2), 0)
|
||||
|
||||
self._create_invoice_for_po(purchase_order)
|
||||
self.assertEqual(purchase_order.invoice_status, 'invoiced')
|
||||
# The section "purchase_order" should now appear because purchase_order was invoiced.
|
||||
# The purchase order of the main company has been billed. Its total should now be in the 'billed' section.
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('purchase_order', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['purchase_order'], items['data'][0]['sequence'])
|
||||
self.assertEqual(float_compare(-self.product_order.standard_price * analytic_contribution * 0.6, items['data'][0]['to_bill'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_order.standard_price * analytic_contribution * 3, items['data'][0]['billed'], 2), 0)
|
||||
self.assertEqual('other_purchase_costs', items['data'][1]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][1]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][1]['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['data'][1]['billed'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_order.standard_price * analytic_contribution * 0.6, items['total']['to_bill'], 2), 0)
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6 - self.product_order.standard_price * analytic_contribution * 3, items['total']['billed'], 2), 0)
|
||||
|
||||
self._create_invoice_for_po(purchase_order_foreign)
|
||||
self.assertEqual(purchase_order_foreign.invoice_status, 'invoiced')
|
||||
# The purchase order of the main company has been billed. Its total should now be in the 'billed' section.
|
||||
# The 'to bill' section of the purchase order should now be empty
|
||||
items = project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual('purchase_order', items['data'][0]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['purchase_order'], items['data'][0]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][0]['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_order.standard_price * analytic_contribution * 3.6, items['data'][0]['billed'], 2), 0)
|
||||
self.assertEqual('other_purchase_costs', items['data'][1]['id'])
|
||||
self.assertEqual(project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], items['data'][1]['sequence'])
|
||||
self.assertEqual(0.0, items['data'][1]['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6, items['data'][1]['billed'], 2), 0)
|
||||
self.assertEqual(0.0, items['total']['to_bill'])
|
||||
self.assertEqual(float_compare(-self.product_a.standard_price * analytic_contribution * 3.6 - self.product_order.standard_price * analytic_contribution * 3.6, items['total']['billed'], 2), 0)
|
||||
|
||||
def test_project_purchase_order_smart_button(self):
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test Project'
|
||||
})
|
||||
|
||||
purchase_order = self.env['purchase.order'].create({
|
||||
"name": "A purchase order",
|
||||
"partner_id": self.partner_a.id,
|
||||
"company_id": self.env.company.id,
|
||||
"order_line": [Command.create({
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 1,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.foreign_currency.id,
|
||||
})],
|
||||
"project_id": project.id,
|
||||
})
|
||||
|
||||
action = project.action_open_project_purchase_orders()
|
||||
self.assertTrue(action)
|
||||
self.assertEqual(action['res_id'], purchase_order.id)
|
||||
|
||||
def test_analytic_distribution_with_included_tax(self):
|
||||
"""When calculating the profitability of a project, included taxes should not be calculated"""
|
||||
included_tax = self.env['account.tax'].create({
|
||||
|
|
@ -296,7 +617,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
'amount': '15.0',
|
||||
'amount_type': 'percent',
|
||||
'type_tax_use': 'purchase',
|
||||
'price_include': True
|
||||
'price_include_override': 'tax_included',
|
||||
})
|
||||
|
||||
# create a purchase.order with the project account in analytic_distribution
|
||||
|
|
@ -307,13 +628,13 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
'analytic_distribution': {self.analytic_account.id: 100},
|
||||
'product_id': self.product_order.id,
|
||||
'product_qty': 2, # plural value to check if the price is multiplied more than once
|
||||
'taxes_id': [included_tax.id], # set the included tax
|
||||
'tax_ids': [included_tax.id], # set the included tax
|
||||
'price_unit': self.product_order.standard_price,
|
||||
'currency_id': self.env.company.currency_id.id,
|
||||
})],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
purchase_order.order_line.flush_recordset()
|
||||
purchase_order.action_create_invoice()
|
||||
# the profitability should not take taxes into account
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['costs'],
|
||||
|
|
@ -331,7 +652,6 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
},
|
||||
)
|
||||
|
||||
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()
|
||||
|
|
@ -368,8 +688,8 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
})
|
||||
purchase_order.button_confirm()
|
||||
# changing the uom to a higher number
|
||||
purchase_order.order_line.product_uom = self.env.ref("uom.product_uom_dozen")
|
||||
purchase_order.order_line.flush_recordset()
|
||||
purchase_order.order_line.product_uom_id = self.env.ref("uom.product_uom_dozen")
|
||||
purchase_order.action_create_invoice()
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['costs'],
|
||||
{
|
||||
|
|
@ -386,6 +706,104 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
},
|
||||
)
|
||||
|
||||
def test_cross_analytics_contribution(self):
|
||||
cross_plan = self.env['account.analytic.plan'].create({'name': 'Cross Plan'})
|
||||
cross_account = self.env['account.analytic.account'].create({
|
||||
'name': "Cross Analytic Account",
|
||||
'plan_id': cross_plan.id,
|
||||
"company_id": self.env.company.id,
|
||||
})
|
||||
cross_distribution = 42
|
||||
|
||||
cross_order = self.env['purchase.order'].create({
|
||||
'name': 'Cross Purchase Order',
|
||||
"partner_id": self.partner_a.id,
|
||||
"company_id": self.env.company.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'analytic_distribution': {
|
||||
f"{self.project.account_id.id},{cross_account.id}": cross_distribution,
|
||||
},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 1,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cross_order.button_confirm()
|
||||
cross_order.action_create_invoice()
|
||||
items = self.project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertEqual(
|
||||
items['data'][0]['to_bill'],
|
||||
-(self.product_order.standard_price * cross_distribution / 100)
|
||||
)
|
||||
|
||||
def test_vendor_credit_note_profitability(self):
|
||||
"""Reversing a vendor bill should cancel out the profitability costs."""
|
||||
purchase_order = self.env['purchase.order'].create({
|
||||
'name': "A Purchase",
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [Command.create({
|
||||
'analytic_distribution': {self.analytic_account.id: 100},
|
||||
'product_id': self.product_order.id,
|
||||
})],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
vendor_bill = self._create_invoice_for_po(purchase_order)
|
||||
|
||||
items = self.project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertDictEqual(items['total'], {
|
||||
'billed': -purchase_order.amount_untaxed,
|
||||
'to_bill': 0.0,
|
||||
})
|
||||
|
||||
credit_note = vendor_bill._reverse_moves()
|
||||
items = self.project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertDictEqual(items['total'], {
|
||||
'billed': -purchase_order.amount_untaxed,
|
||||
'to_bill': purchase_order.amount_untaxed,
|
||||
})
|
||||
|
||||
credit_note.invoice_date = vendor_bill.invoice_date
|
||||
credit_note.action_post()
|
||||
items = self.project._get_profitability_items(with_action=False)['costs']
|
||||
self.assertDictEqual(items['total'], {
|
||||
'billed': 0.0,
|
||||
'to_bill': 0.0,
|
||||
})
|
||||
|
||||
def test_project_purchase_profitability_without_analytic_distribution(self):
|
||||
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,
|
||||
})],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
|
||||
vendor_bill = self._create_invoice_for_po(purchase_order)
|
||||
vendor_bill.invoice_line_ids.analytic_distribution = False
|
||||
|
||||
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': -235.0,
|
||||
'billed': 0.0,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': -235.0,
|
||||
'billed': 0.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_profitability_foreign_currency_rate_on_bill_date(self):
|
||||
"""Test that project profitability uses the correct currency rate (on bill date) for vendor bills in foreign currency."""
|
||||
CurrencyRate = self.env['res.currency.rate']
|
||||
|
|
@ -452,3 +870,164 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch
|
|||
float_compare(actual_billed, expected_cost, precision_digits=2) == 0,
|
||||
f"Expected billed {expected_cost}, got {actual_billed}"
|
||||
)
|
||||
|
||||
def test_project_purchase_profitability_with_split_bills(self):
|
||||
"""
|
||||
Test that project profitability is correctly computed when a purchase order
|
||||
is billed in multiple steps (e.g. one partial bill followed by a final bill
|
||||
covering the remaining quantity)
|
||||
"""
|
||||
|
||||
purchase_order = self.env['purchase.order'].create({
|
||||
'name': "A purchase order",
|
||||
'partner_id': self.partner_a.id,
|
||||
'company_id': self.env.company.id,
|
||||
'order_line': [Command.create({
|
||||
'product_id': self.product_order.id,
|
||||
'price_unit': 100,
|
||||
'tax_ids': [],
|
||||
'product_qty': 5.0,
|
||||
})],
|
||||
'project_id': self.project.id,
|
||||
})
|
||||
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': -500.0,
|
||||
'billed': 0.0,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': -500.0,
|
||||
'billed': 0.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
purchase_order.action_create_invoice()
|
||||
vendor_bill = purchase_order.invoice_ids
|
||||
vendor_bill.invoice_date = datetime.today()
|
||||
vendor_bill.invoice_line_ids.quantity = 2.0
|
||||
vendor_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': -300.0,
|
||||
'billed': -200.0,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': -300.0,
|
||||
'billed': -200.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
purchase_order.action_create_invoice()
|
||||
vendor_bill_2 = purchase_order.invoice_ids[1]
|
||||
vendor_bill_2.invoice_date = datetime.today()
|
||||
vendor_bill_2.invoice_line_ids.quantity = 3.0
|
||||
vendor_bill_2.invoice_line_ids.analytic_distribution = {}
|
||||
vendor_bill_2.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': -300.0,
|
||||
'billed': -200.0,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': -300.0,
|
||||
'billed': -200.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
purchase_order.action_create_invoice()
|
||||
vendor_bill_3 = purchase_order.invoice_ids[2]
|
||||
vendor_bill_3.invoice_date = datetime.today()
|
||||
vendor_bill_3.invoice_line_ids.quantity = 3.0
|
||||
vendor_bill_3.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': -500.0,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': -500.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_project_profitability_when_multiple_aa_in_the_same_line(self):
|
||||
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
|
||||
f"{self.analytic_account.id},{other_analytic_account.id}": 100,
|
||||
},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 1,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
}),
|
||||
Command.create({
|
||||
"analytic_distribution": {
|
||||
# this is the analytic_account that is linked to the project
|
||||
f"{other_analytic_account.id},{self.analytic_account.id}": 100,
|
||||
},
|
||||
"product_id": self.product_order.id,
|
||||
"product_qty": 1,
|
||||
"price_unit": self.product_order.standard_price,
|
||||
"currency_id": self.env.company.currency_id.id,
|
||||
}),
|
||||
],
|
||||
})
|
||||
purchase_order.button_confirm()
|
||||
|
||||
purchase_order.action_create_invoice()
|
||||
|
||||
vendor_bill = purchase_order.invoice_ids[0]
|
||||
vendor_bill.invoice_date = datetime.today()
|
||||
vendor_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': -470.0,
|
||||
}],
|
||||
'total': {
|
||||
'to_bill': 0.0,
|
||||
'billed': -470.0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue