297 lines
10 KiB
Python
297 lines
10 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.freeze_panes, "B1")
|
|
self.assertEqual(summary_sheet["A1"].value, "Workspace Report")
|
|
self.assertEqual(summary_sheet["B1"].value, "Exports")
|
|
self.assertEqual(summary_sheet["A15"].value, "Users Summary")
|
|
merged_ranges = {str(item) for item in summary_sheet.merged_cells.ranges}
|
|
self.assertIn("A15:F15", merged_ranges)
|
|
self.assertIn("H15:J15", merged_ranges)
|
|
self.assertIn("L15:N15", merged_ranges)
|
|
self.assertIn("P15:R15", merged_ranges)
|
|
self.assertNotIn("A15:R15", merged_ranges)
|
|
self.assertIsNone(summary_sheet["G15"].fill.fill_type)
|
|
self.assertIsNone(summary_sheet["G16"].fill.fill_type)
|
|
self.assertIsNone(summary_sheet["K15"].fill.fill_type)
|
|
self.assertIsNone(summary_sheet["O15"].fill.fill_type)
|
|
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))
|
|
self.assertEqual(summary_sheet["A17"].border.top.style, "medium")
|
|
self.assertEqual(summary_sheet["A18"].border.top.style, "medium")
|
|
self.assertIsNone(summary_sheet["G17"].border.top)
|
|
|
|
user_sheet = workbook[workbook.sheetnames[1]]
|
|
user_values = list(user_sheet.iter_rows(values_only=True))
|
|
|
|
self.assertEqual(user_sheet.freeze_panes, "B1")
|
|
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")
|