dodil/erp-brick-timesheets

DODIL ERP brick: timesheets

src/brick_timesheets/domain/hours.py

"""Project hours (TS-09): what a project lead reads to see whether the project is making money while there is time to act.

Plain Python. Everything comes from this module's own rows (never ar's or the order's): the hours of every week's STANDING approved revision (the latest one
approved - a reopened week's earlier revision stands until its correction is approved), what is submitted and not yet approved, what released ar runs have
billed, and the cost the lines carry (hours x the cost rate copied at submission, each line rounded once)."""
from decimal import Decimal

from brick_timesheets.domain.money import ZERO, hours4, money


def _bucket() -> dict:
    return {"approved": Decimal(0), "billable": Decimal(0), "billed": Decimal(0), "cost": Decimal(0)}


def project_hours(rows: list, standing: dict, released: dict) -> dict:
    """rows: one per entry {sheet_id, revision, status, approved_at, person_id, line_no, item_id, billable, hours, unit_cost, currency} of the project's lines
    in the dates asked; standing: sheet_id -> the revision that stands; released: (sheet_id, line_no) -> hours released ar runs billed.
    Returns the totals and the same per item and per person."""
    total, items, people = _bucket(), {}, {}
    waiting = Decimal(0)
    lines: dict = {}
    currencies = set()
    for r in rows:
        if r["status"] == "submitted" and r["approved_at"] is None:
            waiting += Decimal(r["hours"])
        if r["approved_at"] is None or standing.get(r["sheet_id"]) != r["revision"]:
            continue
        key = (r["sheet_id"], r["line_no"])
        ln = lines.setdefault(key, {"item": r["item_id"] or "", "person": str(r["person_id"]), "billable": bool(r["billable"]), "hours": Decimal(0), "cost": Decimal(0)})
        h = Decimal(r["hours"])
        ln["hours"] += h
        if r["unit_cost"] is not None:
            ln["cost"] += h * Decimal(r["unit_cost"])
        if r.get("currency"):
            currencies.add(r["currency"])
    for key, ln in lines.items():
        cost = money(ln["cost"])                                              # a line's cost is rounded once
        billed = Decimal(released.get(key, 0))
        for b in (total, items.setdefault(ln["item"], _bucket()), people.setdefault(ln["person"], _bucket())):
            b["approved"] += ln["hours"]
            b["billable"] += ln["hours"] if ln["billable"] else Decimal(0)
            b["billed"] += billed
            b["cost"] += cost
    return {"total": total, "waiting": waiting, "items": items, "people": people, "currency": next(iter(currencies)) if len(currencies) == 1 else None}


def shown(b: dict, fmt, money_fmt) -> dict:
    return {"approved": fmt(b["approved"]), "billable": fmt(b["billable"]), "billed": fmt(b["billed"]), "unbilled": fmt(b["billable"] - b["billed"]),
            "cost": money_fmt(b["cost"])}


__all__ = ["project_hours", "shown", "ZERO", "hours4"]