feat(reports): add daily rate to report tables and exports

This commit is contained in:
2026-04-28 20:26:20 +03:30
parent 1cd948592c
commit ef05f0a89e
5 changed files with 195 additions and 26 deletions

View File

@@ -81,6 +81,15 @@ def _serialize_money_totals(values: dict[str, Decimal]) -> list[dict]:
]
def _serialize_rate(amount: Decimal | None, currency: str | None) -> dict | None:
if amount is None:
return None
return {
"amount": f"{Decimal(amount).quantize(Decimal('0.01'))}",
"currency": currency or "USD",
}
def _add_income(bucket: defaultdict[str, Decimal], entry: TimeEntry):
if not entry.is_billable or not entry.hourly_rate:
return
@@ -438,17 +447,18 @@ def _scope_payload(filters: ReportFilters) -> dict:
def _table_report_payload(filters: ReportFilters, entries: list[TimeEntry]) -> dict:
summary = _summary_from_entries(entries)
include_latest_rate = not (filters.is_workspace_scope and not filters.user_id)
return {
"scope": _scope_payload(filters),
"summary": summary,
"days": _group_daily(entries),
"days": _group_daily(entries, include_latest_rate=include_latest_rate),
"clients": _build_breakdown(entries, "clients"),
"projects": _build_breakdown(entries, "projects"),
"tags": _build_breakdown(entries, "tags"),
}
def _group_daily(entries: list[TimeEntry]) -> list[dict]:
def _group_daily(entries: list[TimeEntry], *, include_latest_rate: bool) -> list[dict]:
by_day: dict[str, dict] = {}
for entry in entries:
local_start = _localize_datetime(entry.start_time)
@@ -461,6 +471,9 @@ def _group_daily(entries: list[TimeEntry]) -> list[dict]:
"non_billable_seconds": 0,
"total_seconds": 0,
"income": _money_map(),
"latest_rate_amount": None,
"latest_rate_currency": None,
"latest_rate_timestamp": None,
},
)
duration_seconds = get_entry_duration_seconds(entry)
@@ -470,6 +483,18 @@ def _group_daily(entries: list[TimeEntry]) -> list[dict]:
else:
day_bucket["non_billable_seconds"] += duration_seconds
_add_income(day_bucket["income"], entry)
if (
include_latest_rate
and entry.is_billable
and entry.hourly_rate
and (
day_bucket["latest_rate_timestamp"] is None
or local_start >= day_bucket["latest_rate_timestamp"]
)
):
day_bucket["latest_rate_amount"] = Decimal(entry.hourly_rate)
day_bucket["latest_rate_currency"] = entry.currency or "USD"
day_bucket["latest_rate_timestamp"] = local_start
rows = []
for day_key in sorted(by_day.keys()):
@@ -483,6 +508,10 @@ def _group_daily(entries: list[TimeEntry]) -> list[dict]:
"billable_duration": _format_duration_seconds(bucket["billable_seconds"]),
"non_billable_duration": _format_duration_seconds(bucket["non_billable_seconds"]),
"total_duration": _format_duration_seconds(bucket["total_seconds"]),
"latest_hourly_rate": _serialize_rate(
bucket["latest_rate_amount"],
bucket["latest_rate_currency"],
) if include_latest_rate else None,
"income_totals": _serialize_money_totals(bucket["income"]),
}
)