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 .charity_restricted_funds_types import Fund, FundAllocation, FundDraw, FundSpend
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
def _same_currency(currency: str, amount: Money) -> None:
if amount.currency != currency:
raise ValueError("currency mismatch: %s and %s" % (amount.currency, currency))
def allocate_fund_spend(funds: Sequence[Fund], spends: Sequence[FundSpend]) -> FundAllocation:
"""Charge spending to funds the way charity fund accounting requires: a
spend for a restricted purpose uses that purpose's restricted funds first
(in the order given), and only what they cannot cover falls on the
unrestricted funds. General spending never touches a restricted fund, and
no fund may go below zero.
"""
if len(funds) > 0:
currency = funds[0].balance.currency
elif len(spends) > 0:
currency = spends[0].amount.currency
else:
currency = "GBP"
seen = set()
for fund in funds:
if fund.id in seen:
raise ValueError('duplicate fund id "%s"' % (fund.id,))
seen.add(fund.id)
_same_currency(currency, fund.balance)
if fund.restricted and fund.purpose is None:
raise ValueError('restricted fund "%s" needs a purpose' % (fund.id,))
if not fund.restricted and fund.purpose is not None:
raise ValueError('unrestricted fund "%s" must not have a purpose' % (fund.id,))
if fund.balance.minor < 0:
raise ValueError('fund "%s" balance must not be negative, received %d' % (fund.id, fund.balance.minor))
for spend in spends:
_same_currency(currency, spend.amount)
if spend.amount.minor < 0:
raise ValueError('spend "%s" amount must not be negative, received %d' % (spend.id, spend.amount.minor))
balances = [f.balance.minor for f in funds]
draws: List[FundDraw] = []
for spend in spends:
remaining = spend.amount.minor
order = []
if spend.purpose is not None:
order += [i for i, f in enumerate(funds) if f.restricted and f.purpose == spend.purpose]
order += [i for i, f in enumerate(funds) if not f.restricted]
for i in order:
amount = min(remaining, balances[i])
if amount <= 0:
continue
balances[i] -= amount
remaining -= amount
draws.append(FundDraw(spend_id=spend.id, fund_id=funds[i].id, amount=money(amount, currency)))
if remaining > 0:
raise ValueError('insufficient funds for spend "%s": short by %d' % (spend.id, remaining))
return FundAllocation(
draws=draws,
funds=[
Fund(id=f.id, restricted=f.restricted, purpose=f.purpose, balance=money(balances[i], currency))
for i, f in enumerate(funds)
],
)