Files
qlockify-backend-deployment/apps/reports/tests/test_exporters.py
Amirhossein Khalili 20874b9968
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled
feat(reports): improve summary rates and export formatting
2026-05-26 12:15:44 +03:30

283 lines
9.7 KiB
Python

from io import BytesIO
from django.test import TestCase
from openpyxl import load_workbook
from apps.reports.services.export_i18n import build_export_locale
from apps.reports.services.exporters import (
_pdf_summary_rate_label,
_rate_label,
_sort_breakdown_rows,
build_excel_report,
build_pdf_report,
)
def make_report_data(*, user_name="Owner User", mobile="09129990001", hourly_rate=None):
return {
"scope": {
"workspace": {"name": "Exports", "thumbnail_path": None},
"period": "this_month",
"from_date": "2026-04-01",
"to_date": "2026-04-30",
"user": {"name": user_name, "mobile": mobile} if user_name else None,
},
"summary": {
"total_duration": "02:00:00",
"billable_duration": "02:00:00",
"non_billable_duration": "00:00:00",
"income_totals": [{"amount": "30.00", "currency": "USD"}],
},
"days": [
{
"date": "2026-04-12",
"billable_duration": "02:00:00",
"non_billable_duration": "00:00:00",
"total_duration": "02:00:00",
"latest_hourly_rate": hourly_rate,
"income_totals": [{"amount": "30.00", "currency": "USD"}],
}
],
"clients": [
{
"name": "Acme",
"billable_duration": "02:00:00",
"non_billable_duration": "00:00:00",
"total_duration": "02:00:00",
"income_totals": [{"amount": "30.00", "currency": "USD"}],
}
],
"projects": [],
"tags": [],
}
def make_user_summary(*, name: str, mobile: str):
return {
"user": {"id": mobile, "name": name, "mobile": mobile},
"hourly_rates": [{"amount": "15.00", "currency": "USD"}],
"rate_periods": [
{
"amount": "15.00",
"currency": "USD",
"from_date": "2026-04-01",
"to_date": "2026-04-30",
}
],
"total_seconds": 7200,
"total_duration": "02:00:00",
"billable_seconds": 7200,
"billable_duration": "02:00:00",
"non_billable_seconds": 0,
"non_billable_duration": "00:00:00",
"income_totals": [{"amount": "30.00", "currency": "USD"}],
"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"}],
}
def make_variable_user_summary(*, name: str, mobile: str):
summary = make_user_summary(name=name, mobile=mobile)
summary["hourly_rates"] = [
{"amount": "15.00", "currency": "USD"},
{"amount": "18.00", "currency": "USD"},
]
summary["rate_periods"] = [
{
"amount": "15.00",
"currency": "USD",
"from_date": "2026-04-01",
"to_date": "2026-04-14",
},
{
"amount": "18.00",
"currency": "USD",
"from_date": "2026-04-15",
"to_date": "2026-04-30",
},
]
return summary
class ReportExporterTests(TestCase):
def test_export_rate_labels_trim_rial_and_toman_decimals(self):
locale = build_export_locale("en")
self.assertEqual(
_rate_label(locale, {"amount": "1250.75", "currency": "USD"}),
"1,250.75 USD",
)
self.assertEqual(
_rate_label(locale, {"amount": "1250.75", "currency": "IRR"}),
"1,250 IRR",
)
self.assertEqual(
_rate_label(locale, {"amount": "9800.50", "currency": "IRT"}),
"9,800 IRT",
)
def test_pdf_summary_uses_multiple_rates_label(self):
locale = build_export_locale("en")
self.assertEqual(
_pdf_summary_rate_label(
locale,
[
{"amount": "15.00", "currency": "USD"},
{"amount": "18.00", "currency": "USD"},
],
),
"Variable rate",
)
def test_breakdown_rows_are_sorted_by_hour_percentage(self):
rows = [
{"id": "low", "name": "Low", "billable_seconds": 7200},
{"id": "high", "name": "High", "billable_seconds": 3600},
{"id": "tie", "name": "Tie", "billable_seconds": 10800},
]
percentages = [
{"id": "low", "name": "Low", "percentage": "20"},
{"id": "high", "name": "High", "percentage": "70"},
{"id": "tie", "name": "Tie", "percentage": "20"},
]
self.assertEqual(
[row["name"] for row in _sort_breakdown_rows(rows, percentages)],
["High", "Tie", "Low"],
)
def test_excel_export_adds_per_user_sheets_and_daily_rate_column(self):
locale = build_export_locale("en")
report_data = make_report_data(
hourly_rate={"amount": "15.00", "currency": "USD"},
)
report_data["user_summaries"] = [
make_variable_user_summary(name="Owner User", mobile="09129990001"),
make_user_summary(name="Team Mate", mobile="09129990002"),
]
per_user_reports = [
{
**make_report_data(
user_name="Owner User",
mobile="09129990001",
hourly_rate={"amount": "15.00", "currency": "USD"},
),
"user_summary": make_variable_user_summary(name="Owner User", mobile="09129990001"),
},
{
**make_report_data(
user_name="Team Mate",
mobile="09129990002",
hourly_rate={"amount": "15.00", "currency": "USD"},
),
"user_summary": make_user_summary(name="Team Mate", mobile="09129990002"),
},
]
workbook = load_workbook(
BytesIO(
build_excel_report(
report_data=report_data,
locale=locale,
per_user_reports=per_user_reports,
)
)
)
self.assertEqual(workbook.sheetnames[0], "Overall Report")
self.assertIn("Owner User", workbook.sheetnames[1])
self.assertIn("Team Mate", workbook.sheetnames[2])
summary_sheet = workbook[workbook.sheetnames[0]]
summary_values = list(summary_sheet.iter_rows(values_only=True))
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:R15", {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][:18],
(
"Name",
"Mobile",
"Working hours",
"Hourly rate",
"Period",
"Income",
None,
"Clients",
"Hour %",
"Income %",
None,
"Projects",
"Hour %",
"Income %",
None,
"Tags",
"Hour %",
"Income %",
),
)
self.assertTrue(any(row and "Owner User" in row for row in summary_values))
self.assertTrue(any(row and "09129990001" in row for row in summary_values))
self.assertTrue(any(row and "Variable rate" in row for row in summary_values))
user_sheet = workbook[workbook.sheetnames[1]]
user_values = list(user_sheet.iter_rows(values_only=True))
daily_header = next(row[:6] for row in user_values if row and "Date" in row)
self.assertEqual(
daily_header,
(
"Date",
"Billable hours",
"Non-billable hours",
"Total hours",
"Hourly rate",
"Income",
),
)
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[:5],
(
"Name",
"Billable hours",
"Hour %",
"Income",
"Income %",
),
)
def test_pdf_export_supports_persian_locale(self):
locale = build_export_locale("fa")
report_data = make_report_data(
hourly_rate={"amount": "15.00", "currency": "USD"},
)
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",
),
}
]
content = build_pdf_report(report_data=report_data, locale=locale, per_user_reports=per_user_reports)
self.assertEqual(content[:4], b"%PDF")