fix: correct organization analytics aggregation
All checks were successful
All checks were successful
This commit is contained in:
@@ -176,22 +176,27 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
age_under_5_imported=35,
|
||||
age_under_5_utilization_rate=Decimal("92.00"),
|
||||
age_under_5_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_under_5_weighted_load_factor=Decimal("0.78"),
|
||||
age_5_10=41,
|
||||
age_5_10_imported=21,
|
||||
age_5_10_utilization_rate=Decimal("92.00"),
|
||||
age_5_10_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_5_10_weighted_load_factor=Decimal("0.74"),
|
||||
age_10_15=33,
|
||||
age_10_15_imported=17,
|
||||
age_10_15_utilization_rate=Decimal("92.00"),
|
||||
age_10_15_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_10_15_weighted_load_factor=Decimal("0.69"),
|
||||
age_15_20=22,
|
||||
age_15_20_imported=12,
|
||||
age_15_20_utilization_rate=Decimal("92.00"),
|
||||
age_15_20_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_15_20_weighted_load_factor=Decimal("0.63"),
|
||||
age_over_20=21,
|
||||
age_over_20_imported=11,
|
||||
age_over_20_utilization_rate=Decimal("92.00"),
|
||||
age_over_20_lease_share_itn_percent=Decimal("20.00"),
|
||||
age_over_20_weighted_load_factor=Decimal("0.57"),
|
||||
physical_wear_percent=Decimal("28.40"),
|
||||
utilization_rate=Decimal("92.00"),
|
||||
avg_shift_work=Decimal("1.80"),
|
||||
@@ -317,6 +322,61 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_economics_calculates_roa_and_roe_from_f4_and_latest_f2(self):
|
||||
FormF2RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2025,
|
||||
report_quarter=4,
|
||||
total_assets=Decimal("800000.00"),
|
||||
total_equity=Decimal("400000.00"),
|
||||
)
|
||||
FormF2RecordFactory.create(
|
||||
organization=self.organization,
|
||||
report_year=2026,
|
||||
report_quarter=4,
|
||||
total_assets=Decimal("884828.00"),
|
||||
total_equity=Decimal("442414.00"),
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"
|
||||
"?group=efficiency&from_year=2025&to_year=2026"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
ratios_by_period = {row["period"]: row for row in response.data["ratios"]}
|
||||
self.assertEqual(ratios_by_period[2025]["roa"], 50.0)
|
||||
self.assertEqual(ratios_by_period[2025]["roe"], 100.0)
|
||||
self.assertEqual(ratios_by_period[2026]["roa"], 50.0)
|
||||
self.assertEqual(ratios_by_period[2026]["roe"], 100.0)
|
||||
self.assertEqual(
|
||||
response.data["ratios_report_period"],
|
||||
{
|
||||
"financial_form": "F-2",
|
||||
"report_year": 2026,
|
||||
"report_quarter": 4,
|
||||
},
|
||||
)
|
||||
|
||||
def test_economics_returns_null_roa_and_roe_without_f2_denominators(self):
|
||||
organization = OrganizationFactory.create()
|
||||
FormF4RecordFactory.create(
|
||||
organization=organization,
|
||||
report_year=2026,
|
||||
report_half_year=2,
|
||||
net_profit_rsbu=Decimal("1000.00"),
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/analytics/economics/"
|
||||
"?group=efficiency&from_year=2026&to_year=2026"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIsNone(response.data["ratios"][0]["roa"])
|
||||
self.assertIsNone(response.data["ratios"][0]["roe"])
|
||||
self.assertIsNone(response.data["ratios_report_period"])
|
||||
|
||||
def test_economics_respects_requested_range_when_it_is_empty(self):
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/economics/"
|
||||
@@ -480,8 +540,13 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
"utilization_rate",
|
||||
"lease_share_itn_percent",
|
||||
"weighted_wear_percent",
|
||||
"weighted_load_factor",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["age_distribution"][0]["weighted_load_factor"],
|
||||
0.78,
|
||||
)
|
||||
self.assertGreaterEqual(len(response.data["categories"]), 1)
|
||||
self.assertEqual(len(response.data["dynamics_series"]), 3)
|
||||
self.assertEqual(
|
||||
@@ -510,6 +575,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
self.assertEqual(products_response.data["report_year"], 2026)
|
||||
self.assertEqual(products_response.data["frequency"], "quarterly")
|
||||
self.assertEqual(products_response.data["price_mode"], "actual")
|
||||
self.assertEqual(products_response.data["summary_period"], "2026-Q1")
|
||||
self.assertEqual(
|
||||
products_response.data["summary"]["military_output_amount"], 11000000
|
||||
)
|
||||
@@ -567,6 +633,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
)
|
||||
self.assertEqual(semiannual_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(semiannual_response.data["frequency"], "semiannual")
|
||||
self.assertEqual(semiannual_response.data["summary_period"], "2026-H1")
|
||||
self.assertEqual(len(semiannual_response.data["production_series"]), 1)
|
||||
self.assertEqual(
|
||||
semiannual_response.data["production_series"][0]["period"], "2026-H1"
|
||||
@@ -582,6 +649,7 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
)
|
||||
self.assertEqual(monthly_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(monthly_response.data["frequency"], "monthly")
|
||||
self.assertEqual(monthly_response.data["summary_period"], "2026-06")
|
||||
self.assertEqual(len(monthly_response.data["production_series"]), 6)
|
||||
self.assertEqual(
|
||||
monthly_response.data["production_series"][0]["period"], "2026-01"
|
||||
@@ -591,6 +659,104 @@ class OrganizationAnalyticsApiTest(APITestCase):
|
||||
3666666,
|
||||
)
|
||||
|
||||
def test_products_aggregates_real_monthly_f1_records_chronologically(self):
|
||||
organization = OrganizationFactory.create()
|
||||
for month in (7, 4, 5, 2, 1, 3, 6):
|
||||
amount = Decimal(month) * Decimal("1000000.00")
|
||||
FormF1RecordFactory.create(
|
||||
organization=organization,
|
||||
report_year=2026,
|
||||
report_month=month,
|
||||
report_quarter=None,
|
||||
military_output_actual=amount,
|
||||
civilian_output_actual=amount,
|
||||
hightech_output_actual=amount,
|
||||
rd_volume_actual=amount,
|
||||
military_domestic_actual=amount,
|
||||
military_export_actual=amount,
|
||||
civilian_domestic_actual=amount,
|
||||
civilian_export_actual=amount,
|
||||
)
|
||||
|
||||
monthly_response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/analytics/products/"
|
||||
"?frequency=monthly&price_mode=actual&report_year=2026"
|
||||
)
|
||||
quarterly_response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/analytics/products/"
|
||||
"?frequency=quarterly&price_mode=actual&report_year=2026"
|
||||
)
|
||||
semiannual_response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/analytics/products/"
|
||||
"?frequency=semiannual&price_mode=actual&report_year=2026"
|
||||
)
|
||||
annual_response = self.client.get(
|
||||
f"/api/v1/organizations/{organization.id}/analytics/products/"
|
||||
"?frequency=annual&price_mode=actual&report_year=2026"
|
||||
)
|
||||
|
||||
for response in (
|
||||
monthly_response,
|
||||
quarterly_response,
|
||||
semiannual_response,
|
||||
annual_response,
|
||||
):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
self.assertEqual(
|
||||
[row["period"] for row in monthly_response.data["production_series"]],
|
||||
[
|
||||
"2026-01",
|
||||
"2026-02",
|
||||
"2026-03",
|
||||
"2026-04",
|
||||
"2026-05",
|
||||
"2026-06",
|
||||
"2026-07",
|
||||
],
|
||||
)
|
||||
self.assertEqual(monthly_response.data["summary_period"], "2026-07")
|
||||
self.assertEqual(
|
||||
monthly_response.data["summary"]["military_output_amount"],
|
||||
7000000,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[row["period"] for row in quarterly_response.data["production_series"]],
|
||||
["2026-Q1", "2026-Q2", "2026-Q3"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
row["military_output_amount"]
|
||||
for row in quarterly_response.data["production_series"]
|
||||
],
|
||||
[6000000, 15000000, 7000000],
|
||||
)
|
||||
self.assertEqual(quarterly_response.data["summary_period"], "2026-Q3")
|
||||
|
||||
self.assertEqual(
|
||||
[row["period"] for row in semiannual_response.data["production_series"]],
|
||||
["2026-H1", "2026-H2"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
row["military_output_amount"]
|
||||
for row in semiannual_response.data["production_series"]
|
||||
],
|
||||
[21000000, 7000000],
|
||||
)
|
||||
self.assertEqual(semiannual_response.data["summary_period"], "2026-H2")
|
||||
|
||||
self.assertEqual(
|
||||
[row["period"] for row in annual_response.data["production_series"]],
|
||||
["2026"],
|
||||
)
|
||||
self.assertEqual(
|
||||
annual_response.data["production_series"][0]["military_output_amount"],
|
||||
28000000,
|
||||
)
|
||||
self.assertEqual(annual_response.data["summary_period"], "2026")
|
||||
|
||||
def test_forecast_contract(self):
|
||||
response = self.client.get(
|
||||
f"/api/v1/organizations/{self.organization.id}/analytics/forecast/"
|
||||
|
||||
Reference in New Issue
Block a user