Files

104 lines
4.9 KiB
Python
Raw Permalink Normal View History

2026-10-29 20:40:02 +00:00
from decimal import Decimal
from .calc import round2
from .models import BudgetYear, Category, Department, Site, User, WorkflowStep
from .permissions import allowed_actions, steps_of
from .summary import LineView, YearView
def money(value: Decimal | None) -> float | None:
return None if value is None else float(round2(value))
def pct(value: Decimal | None) -> float | None:
return None if value is None else float(Decimal(value).quantize(Decimal("0.01")))
def user_out(u: User) -> dict:
return {"id": u.id, "email": u.email, "name": u.name, "role": u.role, "language": u.language,
"active": u.active, "department_id": u.department_id,
"department": u.department.name if u.department else None, "theme": u.theme or "system",
"mfa_enabled": bool(u.mfa_enabled), "mfa_enabled_at": u.mfa_enabled_at.isoformat() if u.mfa_enabled_at else None,
"mfa_recovery_left": len(u.mfa_recovery or []) if u.mfa_enabled else 0,
# the timestamp changes the picture's URL, so browsers fetch a new one after a change
"avatar_url": f"/api/users/{u.id}/avatar?v={int(u.avatar_updated_at.timestamp())}" if u.avatar_updated_at else None}
def step_out(s: WorkflowStep) -> dict:
return {"id": s.id, "position": s.position, "name": s.name, "approver_mode": s.approver_mode,
"approver_ids": s.approver_ids or []}
def department_out(d: Department) -> dict:
return {"id": d.id, "name": d.name, "active": d.active, "steps": [step_out(s) for s in steps_of(d)]}
def site_out(s: Site) -> dict:
return {"id": s.id, "name": s.name, "active": s.active}
def category_out(c: Category) -> dict:
return {"id": c.id, "department_id": c.department_id, "name_en": c.name_en, "name_pt": c.name_pt, "section": c.section,
"sort_order": c.sort_order, "show_when_empty": c.show_when_empty, "active": c.active}
def totals_out(t) -> dict:
return {"prior": money(t.prior), "base": money(t.base), "adjusted": money(t.adjusted),
"change": money(t.change), "change_pct": pct(t.change_pct), "months": [money(m) for m in t.months]}
def line_out(lv: LineView) -> dict:
line, f = lv.line, lv.figures
return {
"id": line.id, "category_id": line.category_id, "site_id": line.site_id, "name": line.name,
"description": line.description, "remarks": line.remarks,
"prior_expr": line.prior_expr, "base_expr": line.base_expr,
"adj_kind": line.adj_kind, "adj_value": float(line.adj_value or 0), "adj_reason": line.adj_reason,
"monthly_kind": line.monthly_kind, "monthly_values": line.monthly_values,
"monthly_months": line.monthly_months,
"sort_order": line.sort_order,
"prior": money(f.prior), "base": money(f.base), "factor": float(f.factor), "adjusted": money(f.adjusted),
"change": money(f.change), "change_pct": pct(f.change_pct), "months": [money(m) for m in f.months],
}
def year_out(y: BudgetYear, view: YearView | None = None, user: User | None = None) -> dict:
out = {
"id": y.id, "year": y.year, "title": y.title, "default_rate": float(y.default_rate or 0),
"department_id": y.department_id, "department": y.department.name,
"status": y.status, "notes": y.notes, "current_step": y.current_step,
"workflow": [step_out(s) for s in steps_of(y.department)],
"submitted_by": y.submitted_by.name if y.submitted_by else None,
"submitted_at": y.submitted_at.isoformat() if y.submitted_at else None,
"approved_by": y.approved_by.name if y.approved_by else None,
"approved_at": y.approved_at.isoformat() if y.approved_at else None,
"source_file": y.source_file or "", "archived": bool(y.archived),
"archived_at": y.archived_at.isoformat() if y.archived_at else None,
"archived_by": y.archived_by.name if y.archived_by else None,
"reopen_request": next((reopen_out(r) for r in y.reopen_requests if r.status == "pending"), None),
}
if user is not None:
out["actions"] = allowed_actions(user, y)
if view is not None:
out["costs"] = totals_out(view.costs)
out["revenue"] = totals_out(view.revenue)
return out
def reopen_out(r) -> dict:
return {"id": r.id, "status": r.status, "reason": r.reason, "requested_by": r.requested_by.name if r.requested_by else None,
"requested_at": r.requested_at.isoformat() if r.requested_at else None,
"decided_by": r.decided_by.name if r.decided_by else None,
"decided_at": r.decided_at.isoformat() if r.decided_at else None, "decision_note": r.decision_note}
def year_detail(view: YearView, user: User | None = None) -> dict:
out = year_out(view.year, view, user)
out["reopen_requests"] = [reopen_out(r) for r in reversed(view.year.reopen_requests)]
out["categories"] = [
{"category": category_out(cv.category), "totals": totals_out(cv.totals),
"lines": [line_out(lv) for lv in cv.lines]}
for cv in view.categories
]
return out