Functional Weave
Code in Python

finance.accruals-prepayments@1.0.0

impl/python.py

2,069 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import List, Sequence

from .dates_days_between import days_between  ← from dates.days-between ^1.0.0 · built alongside by fune
from .dates_overlap import overlap_days  ← from dates.overlap ^1.0.0 · built alongside by fune
from .finance_accruals_prepayments_types import AccountingPeriod, PeriodAllocation
from .money_allocate import allocate  ← from money.allocate ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune


def allocate_to_periods(
    amount: Money, service_start: str, service_end: str, periods: Sequence[AccountingPeriod]
) -> List[PeriodAllocation]:
    """Split an invoice across accounting periods by days of service.

    money.allocate does the split with each period's days as the weight, so the
    shares add back to the invoice exactly. A negative amount is split as its
    positive and negated, so a credit note mirrors its invoice period by period.
    """
    service_days = days_between(service_start, service_end) + 1
    if service_days < 1:
        raise ValueError("service ends %s, before it starts %s" % (service_end, service_start))
    if len(periods) == 0:
        raise ValueError("at least one accounting period is needed")
    days: List[int] = []
    for index, period in enumerate(periods):
        if days_between(period.start, period.end) < 0:
            raise ValueError("period %s ends before it starts" % (period.label,))
        if index > 0 and days_between(periods[index - 1].end, period.start) < 1:
            raise ValueError("period %s overlaps or precedes the period before it" % (period.label,))
        days.append(overlap_days(service_start, service_end, period.start, period.end, True))
    covered = sum(days)
    if covered != service_days:
        raise ValueError("the accounting periods cover %d of the %d days of service" % (covered, service_days))

    negative = amount.minor < 0
    shares = allocate(money(-amount.minor if negative else amount.minor, amount.currency), days)
    return [
        PeriodAllocation(
            label=period.label,
            days=days[index],
            amount=money(-shares[index].minor, amount.currency) if negative else shares[index],
        )
        for index, period in enumerate(periods)
    ]