feat(reports): add uncategorized dual-share exports

This commit is contained in:
2026-05-21 19:10:33 +03:30
parent e234eac26d
commit 8d2f876c82
5 changed files with 838 additions and 319 deletions

View File

@@ -147,9 +147,130 @@ class ReportViewTests(APITestCase):
self.assertEqual(owner_summary["project_percentages"][0]["percentage"], "100")
self.assertEqual(owner_summary["client_percentages"][0]["percentage"], "100")
self.assertEqual(owner_summary["tag_percentages"][0]["percentage"], "100")
self.assertEqual(member_summary["project_percentages"], [])
self.assertEqual(member_summary["client_percentages"], [])
self.assertEqual(member_summary["tag_percentages"], [])
self.assertEqual(member_summary["project_percentages"][0]["percentage"], "0")
self.assertEqual(member_summary["client_percentages"][0]["percentage"], "0")
self.assertEqual(member_summary["tag_percentages"][0]["percentage"], "0")
def test_specific_user_report_includes_uncategorized_rows_and_balanced_percentages(self):
self.client.force_authenticate(user=self.owner)
TimeEntry.objects.create(
workspace=self.workspace,
user=self.owner,
project=None,
description="Uncategorized billable",
start_time="2026-04-12T10:00:00+03:30",
end_time="2026-04-12T11:00:00+03:30",
duration=timedelta(hours=1),
is_billable=True,
hourly_rate=Decimal("10.00"),
currency="USD",
)
with patch(
"apps.reports.services.aggregation.timezone.localdate",
return_value=date(2026, 4, 20),
):
response = self.client.get(
"/api/reports/table/",
{
"workspace": str(self.workspace.id),
"period": "this_month",
"user": str(self.owner.id),
"language": "en",
},
)
self.assertEqual(response.status_code, 200)
summary = response.data["user_summary"]
self.assertEqual(
sum(int(row["percentage"]) for row in summary["project_percentages"]),
100,
)
self.assertEqual(
sum(int(row["percentage"]) for row in summary["client_percentages"]),
100,
)
self.assertEqual(
sum(int(row["percentage"]) for row in summary["tag_percentages"]),
100,
)
self.assertEqual(
{row["name"] for row in summary["project_percentages"]},
{"Website", "No project"},
)
self.assertEqual(
{row["name"] for row in summary["client_percentages"]},
{"Acme", "No client"},
)
self.assertEqual(
{row["name"] for row in summary["tag_percentages"]},
{"Design", "No tag"},
)
self.assertEqual(
{row["name"] for row in response.data["projects"]},
{"Website", "No project"},
)
self.assertEqual(
{row["name"] for row in response.data["clients"]},
{"Acme", "No client"},
)
self.assertEqual(
{row["name"] for row in response.data["tags"]},
{"Design", "No tag"},
)
self.assertEqual(
sum(int(row["percentage"]) for row in summary["project_income_percentages"]),
100,
)
self.assertEqual(
sum(int(row["percentage"]) for row in summary["client_income_percentages"]),
100,
)
self.assertEqual(
sum(int(row["percentage"]) for row in summary["tag_income_percentages"]),
100,
)
def test_income_percentages_are_hidden_for_mixed_currency_breakdowns(self):
self.client.force_authenticate(user=self.owner)
second_project = Project.objects.create(
workspace=self.workspace,
name="Mobile App",
client=self.client_obj,
)
TimeEntry.objects.create(
workspace=self.workspace,
user=self.owner,
project=second_project,
description="EUR work",
start_time="2026-04-13T10:00:00+03:30",
end_time="2026-04-13T11:00:00+03:30",
duration=timedelta(hours=1),
is_billable=True,
hourly_rate=Decimal("20.00"),
currency="EUR",
)
with patch(
"apps.reports.services.aggregation.timezone.localdate",
return_value=date(2026, 4, 20),
):
response = self.client.get(
"/api/reports/table/",
{
"workspace": str(self.workspace.id),
"period": "this_month",
"user": str(self.owner.id),
"language": "en",
},
)
self.assertEqual(response.status_code, 200)
summary = response.data["user_summary"]
self.assertEqual(summary["project_income_percentages"], [])
self.assertEqual(summary["client_income_percentages"], [])
def test_daily_rate_uses_latest_billable_entry_snapshot(self):
self.client.force_authenticate(user=self.owner)