feat(reports): add daily rate to report tables and exports
This commit is contained in:
@@ -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"]),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user