dodil/erp-brick-ar

DODIL ERP brick: accounts receivable

src/brick_ar/domain/finance.py

"""The arithmetic of v4, pure: statutory interest and compensation (AR-39) and the settlement-discount split (AR-38)."""
from datetime import date
from decimal import ROUND_HALF_UP, Decimal
from typing import Optional

from brick_ar.domain.errors import RuleBroken
from brick_ar.domain.money import ZERO, money

MARGIN = Decimal(8)          # the Late Payment of Commercial Debts (Interest) Act 1998


def statutory_rate(reference_pct: Decimal, margin_pct: Decimal = MARGIN) -> Decimal:
    return Decimal(reference_pct) + Decimal(margin_pct)


def reference_date(first_late_day: date) -> date:
    """The reference rate is the base rate on 30 June (for a debt whose interest starts July-December) or 31 December (January-June)."""
    return date(first_late_day.year, 6, 30) if first_late_day.month >= 7 else date(first_late_day.year - 1, 12, 31)


def interest(principal: Decimal, rate_pct: Decimal, days: int) -> Decimal:
    """principal x rate x days / 365, rounded ONCE to the penny (simple interest, daily)."""
    if days <= 0:
        return ZERO
    return (Decimal(principal) * Decimal(rate_pct) / 100 * days / 365).quantize(Decimal("0.01"), ROUND_HALF_UP)


def compensation(debt: Decimal) -> Decimal:
    """The fixed sum: 40.00 under 1,000.00, 70.00 from 1,000.00, 100.00 from 10,000.00."""
    d = Decimal(debt)
    return Decimal("100.00") if d >= 10000 else Decimal("70.00") if d >= 1000 else Decimal("40.00")


def discount_shares(rows: list, discount: Decimal) -> list:
    """The settlement discount (gross) split per tax code at the invoice's frozen rates, pro rata to each code's gross.
    `rows` are (tax_code, net, tax); returns [(tax_code, net_to_credit)] where net_to_credit * (1 + rate) is the code's share."""
    gross = sum((n + t for _c, n, t in rows), ZERO)
    if gross <= 0:
        raise RuleBroken("the invoice has no gross to discount")
    out, given = [], ZERO
    for i, (code, net, tax) in enumerate(rows):
        share = discount - given if i == len(rows) - 1 else money(discount * (net + tax) / gross)
        given += share
        out.append((code, money(share * net / (net + tax)) if (net + tax) else ZERO, share))
    return out


def window_open(received_on: date, until: Optional[date]) -> bool:
    return until is not None and received_on <= until