from typing import List, Optional, Sequence from .dates_days_between import days_between from .finance_aged_debt_types import AgedDebt, AgedDebtBucket, OpenInvoice from .money_add import add_money from .money_amount import Money, money def aged_debt(invoices: Sequence[OpenInvoice], as_at: str, bucket_days: Sequence[int], currency: str) -> AgedDebt: """Bucket open invoices by how many days past their due date they are. Age runs from the due date, not the invoice date: an invoice due today is current. Every bucket is returned, empty or not, so a report's columns are fixed. """ if len(bucket_days) == 0: raise ValueError("bucketDays needs at least one boundary") previous = 0 for bound in bucket_days: if isinstance(bound, bool) or not isinstance(bound, int) or bound <= previous: raise ValueError( "bucketDays must be whole days, ascending, from 1: received %s after %s" % (bound, previous) ) previous = bound zero = money(0, currency) labels: List[str] = ["current"] froms: List[Optional[int]] = [None] tos: List[Optional[int]] = [0] start = 1 for bound in bucket_days: labels.append("%d-%d" % (start, bound)) froms.append(start) tos.append(bound) start = bound + 1 labels.append("%d+" % (start - 1,)) froms.append(start) tos.append(None) totals: List[Money] = [zero for _ in labels] refs: List[List[str]] = [[] for _ in labels] total = zero for invoice in invoices: overdue = days_between(invoice.due_date, as_at) index = 0 if overdue > 0: index = len(bucket_days) + 1 for i, bound in enumerate(bucket_days): if overdue <= bound: index = i + 1 break totals[index] = add_money(totals[index], invoice.outstanding) refs[index].append(invoice.reference) total = add_money(total, invoice.outstanding) buckets = [ AgedDebtBucket(label=labels[i], from_days=froms[i], to_days=tos[i], total=totals[i], references=refs[i]) for i in range(len(labels)) ] return AgedDebt(as_at=as_at, buckets=buckets, total=total)