from typing import List, Sequence from .dates_days_between import days_between from .dates_overlap import overlap_days from .finance_accruals_prepayments_types import AccountingPeriod, PeriodAllocation from .money_allocate import allocate from .money_amount import Money, money 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) ]