104 lines
4.9 KiB
Python
104 lines
4.9 KiB
Python
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
|