feat(reports): add uncategorized dual-share exports
This commit is contained in:
@@ -68,6 +68,9 @@ def make_user_summary(*, name: str, mobile: str):
|
||||
"project_percentages": [{"id": "1", "name": "Website", "percentage": "100"}],
|
||||
"client_percentages": [{"id": "1", "name": "Acme", "percentage": "100"}],
|
||||
"tag_percentages": [{"id": "1", "name": "Design", "percentage": "100"}],
|
||||
"project_income_percentages": [{"id": "1", "name": "Website", "percentage": "100"}],
|
||||
"client_income_percentages": [{"id": "1", "name": "Acme", "percentage": "100"}],
|
||||
"tag_income_percentages": [{"id": "1", "name": "Design", "percentage": "100"}],
|
||||
}
|
||||
|
||||
|
||||
@@ -120,23 +123,26 @@ class ReportExporterTests(TestCase):
|
||||
self.assertEqual(summary_sheet["A1"].value, "Workspace Report")
|
||||
self.assertEqual(summary_sheet["B1"].value, "Exports")
|
||||
self.assertEqual(summary_sheet["A15"].value, "Users Summary")
|
||||
self.assertIn("A15:M15", {str(item) for item in summary_sheet.merged_cells.ranges})
|
||||
self.assertIn("A15:P15", {str(item) for item in summary_sheet.merged_cells.ranges})
|
||||
self.assertEqual(
|
||||
tuple(summary_sheet.iter_rows(min_row=16, max_row=16, values_only=True))[0][:13],
|
||||
tuple(summary_sheet.iter_rows(min_row=16, max_row=16, values_only=True))[0][:16],
|
||||
(
|
||||
"Name",
|
||||
"Mobile",
|
||||
"Working hours",
|
||||
"Non-working hours",
|
||||
"Income",
|
||||
"Hourly rate",
|
||||
"Period",
|
||||
"Income",
|
||||
"Clients",
|
||||
"Percentage",
|
||||
"Hour %",
|
||||
"Income %",
|
||||
"Projects",
|
||||
"Percentage",
|
||||
"Hour %",
|
||||
"Income %",
|
||||
"Tags",
|
||||
"Percentage",
|
||||
"Hour %",
|
||||
"Income %",
|
||||
),
|
||||
)
|
||||
self.assertTrue(any(row and "Owner User" in row for row in summary_values))
|
||||
@@ -161,6 +167,20 @@ class ReportExporterTests(TestCase):
|
||||
daily_row = next(row[:6] for row in user_values if row and "2026/04/12" in row)
|
||||
self.assertEqual(daily_row[4], "15 USD")
|
||||
|
||||
breakdown_header = next(row[:7] for row in user_values if row and row[0] == "Name" and row[2] == "Hour %")
|
||||
self.assertEqual(
|
||||
breakdown_header,
|
||||
(
|
||||
"Name",
|
||||
"Billable hours",
|
||||
"Hour %",
|
||||
"Non-billable hours",
|
||||
"Total hours",
|
||||
"Income",
|
||||
"Income %",
|
||||
),
|
||||
)
|
||||
|
||||
def test_pdf_export_supports_persian_locale(self):
|
||||
locale = build_export_locale("fa")
|
||||
report_data = make_report_data(
|
||||
@@ -168,7 +188,16 @@ class ReportExporterTests(TestCase):
|
||||
)
|
||||
report_data["user_summaries"] = [make_user_summary(name="Owner User", mobile="09129990001")]
|
||||
per_user_reports = [
|
||||
{**make_report_data(user_name="Owner User", mobile="09129990001"), "user_summary": make_user_summary(name="Owner User", mobile="09129990001")}
|
||||
{
|
||||
**make_report_data(
|
||||
user_name="Owner User",
|
||||
mobile="09129990001",
|
||||
),
|
||||
"user_summary": make_user_summary(
|
||||
name="Owner User",
|
||||
mobile="09129990001",
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
content = build_pdf_report(report_data=report_data, locale=locale, per_user_reports=per_user_reports)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user