balance_sheet: Add activity report.
This commit is contained in:
parent
89bbf84347
commit
29d4325c7a
3 changed files with 228 additions and 41 deletions
|
@ -22,12 +22,16 @@ import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from typing import (
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
Hashable,
|
Hashable,
|
||||||
Iterable,
|
Iterable,
|
||||||
Iterator,
|
Iterator,
|
||||||
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
Optional,
|
||||||
|
@ -50,14 +54,18 @@ from .. import ranges
|
||||||
PROGNAME = 'balance-sheet-report'
|
PROGNAME = 'balance-sheet-report'
|
||||||
logger = logging.getLogger('conservancy_beancount.tools.balance_sheet')
|
logger = logging.getLogger('conservancy_beancount.tools.balance_sheet')
|
||||||
|
|
||||||
class Fund(enum.Enum):
|
class Fund(enum.IntFlag):
|
||||||
RESTRICTED = enum.auto()
|
RESTRICTED = enum.auto()
|
||||||
UNRESTRICTED = enum.auto()
|
UNRESTRICTED = enum.auto()
|
||||||
|
ANY = RESTRICTED | UNRESTRICTED
|
||||||
|
|
||||||
|
|
||||||
class Period(enum.Enum):
|
class Period(enum.IntFlag):
|
||||||
OPENING = enum.auto()
|
OPENING = enum.auto()
|
||||||
|
PRIOR = enum.auto()
|
||||||
PERIOD = enum.auto()
|
PERIOD = enum.auto()
|
||||||
|
BEFORE_PERIOD = OPENING | PRIOR
|
||||||
|
ANY = OPENING | PRIOR | PERIOD
|
||||||
|
|
||||||
|
|
||||||
class BalanceKey(NamedTuple):
|
class BalanceKey(NamedTuple):
|
||||||
|
@ -81,11 +89,11 @@ class Balances:
|
||||||
fund_key: str='project',
|
fund_key: str='project',
|
||||||
unrestricted_fund_value: str='Conservancy',
|
unrestricted_fund_value: str='Conservancy',
|
||||||
) -> None:
|
) -> None:
|
||||||
self.opening_range = ranges.DateRange(
|
self.prior_range = ranges.DateRange(
|
||||||
cliutil.diff_year(start_date, -1),
|
cliutil.diff_year(start_date, -1),
|
||||||
cliutil.diff_year(stop_date, -1),
|
cliutil.diff_year(stop_date, -1),
|
||||||
)
|
)
|
||||||
assert self.opening_range.stop <= start_date
|
assert self.prior_range.stop <= start_date
|
||||||
self.period_range = ranges.DateRange(start_date, stop_date)
|
self.period_range = ranges.DateRange(start_date, stop_date)
|
||||||
self.balances: Mapping[BalanceKey, core.MutableBalance] \
|
self.balances: Mapping[BalanceKey, core.MutableBalance] \
|
||||||
= collections.defaultdict(core.MutableBalance)
|
= collections.defaultdict(core.MutableBalance)
|
||||||
|
@ -93,34 +101,40 @@ class Balances:
|
||||||
post_date = post.meta.date
|
post_date = post.meta.date
|
||||||
if post_date in self.period_range:
|
if post_date in self.period_range:
|
||||||
period = Period.PERIOD
|
period = Period.PERIOD
|
||||||
elif post_date < self.period_range.start:
|
elif post_date in self.prior_range:
|
||||||
|
period = Period.PRIOR
|
||||||
|
elif post_date < self.prior_range.start:
|
||||||
period = Period.OPENING
|
period = Period.OPENING
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
if post.account == 'Expenses:CurrencyConversion':
|
||||||
|
account = data.Account('Income:CurrencyConversion')
|
||||||
|
else:
|
||||||
|
account = post.account
|
||||||
if post.meta.get(fund_key) == unrestricted_fund_value:
|
if post.meta.get(fund_key) == unrestricted_fund_value:
|
||||||
fund = Fund.UNRESTRICTED
|
fund = Fund.UNRESTRICTED
|
||||||
else:
|
else:
|
||||||
fund = Fund.RESTRICTED
|
fund = Fund.RESTRICTED
|
||||||
try:
|
try:
|
||||||
classification_s = post.account.meta['classification']
|
classification_s = account.meta['classification']
|
||||||
if isinstance(classification_s, str):
|
if isinstance(classification_s, str):
|
||||||
classification = data.Account(classification_s)
|
classification = data.Account(classification_s)
|
||||||
else:
|
else:
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
classification = post.account
|
classification = account
|
||||||
try:
|
try:
|
||||||
post_type = post.meta[self.POST_TYPES[post.account.root_part()]]
|
post_type = post.meta[self.POST_TYPES[account.root_part()]]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
post_type = None
|
post_type = None
|
||||||
key = BalanceKey(post.account, classification, period, fund, post_type)
|
key = BalanceKey(account, classification, period, fund, post_type)
|
||||||
self.balances[key] += post.at_cost()
|
self.balances[key] += post.at_cost()
|
||||||
|
|
||||||
def total(self,
|
def total(self,
|
||||||
account: Optional[str]=None,
|
account: Optional[str]=None,
|
||||||
classification: Optional[str]=None,
|
classification: Optional[str]=None,
|
||||||
period: Optional[Period]=None,
|
period: int=Period.ANY,
|
||||||
fund: Optional[Fund]=None,
|
fund: int=Fund.ANY,
|
||||||
post_type: Optional[str]=None,
|
post_type: Optional[str]=None,
|
||||||
) -> core.Balance:
|
) -> core.Balance:
|
||||||
retval = core.MutableBalance()
|
retval = core.MutableBalance()
|
||||||
|
@ -130,9 +144,9 @@ class Balances:
|
||||||
elif not (classification is None
|
elif not (classification is None
|
||||||
or key.classification.is_under(classification)):
|
or key.classification.is_under(classification)):
|
||||||
pass
|
pass
|
||||||
elif not (period is None or period is key.period):
|
elif not period & key.period:
|
||||||
pass
|
pass
|
||||||
elif not (fund is None or fund is key.fund):
|
elif not fund & key.fund:
|
||||||
pass
|
pass
|
||||||
elif not (post_type is None or post_type == key.post_type):
|
elif not (post_type is None or post_type == key.post_type):
|
||||||
pass
|
pass
|
||||||
|
@ -140,22 +154,37 @@ class Balances:
|
||||||
retval += balance
|
retval += balance
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def classifications(self, account: str) -> Sequence[data.Account]:
|
def classifications(self,
|
||||||
|
account: str,
|
||||||
|
sort_period: int=Period.PERIOD,
|
||||||
|
) -> Sequence[data.Account]:
|
||||||
class_bals: Mapping[data.Account, core.MutableBalance] \
|
class_bals: Mapping[data.Account, core.MutableBalance] \
|
||||||
= collections.defaultdict(core.MutableBalance)
|
= collections.defaultdict(core.MutableBalance)
|
||||||
for key, balance in self.balances.items():
|
for key, balance in self.balances.items():
|
||||||
if key.account.is_under(account):
|
if not key.account.is_under(account):
|
||||||
|
pass
|
||||||
|
elif key.period & sort_period:
|
||||||
class_bals[key.classification] += balance
|
class_bals[key.classification] += balance
|
||||||
|
else:
|
||||||
|
# Ensure the balance exists in the mapping
|
||||||
|
class_bals[key.classification]
|
||||||
norm_func = core.normalize_amount_func(f'{account}:RootsOK')
|
norm_func = core.normalize_amount_func(f'{account}:RootsOK')
|
||||||
def sortkey(acct: data.Account) -> Hashable:
|
def sortkey(acct: data.Account) -> Hashable:
|
||||||
prefix, _, _ = acct.rpartition(':')
|
prefix, _, _ = acct.rpartition(':')
|
||||||
balance = norm_func(class_bals[acct])
|
balance = norm_func(class_bals[acct])
|
||||||
max_bal = max(amount.number for amount in balance.values())
|
try:
|
||||||
|
max_bal = max(amount.number for amount in balance.values())
|
||||||
|
except ValueError:
|
||||||
|
max_bal = Decimal(0)
|
||||||
return prefix, -max_bal
|
return prefix, -max_bal
|
||||||
return sorted(class_bals, key=sortkey)
|
return sorted(class_bals, key=sortkey)
|
||||||
|
|
||||||
|
|
||||||
class Report(core.BaseODS[Sequence[None], None]):
|
class Report(core.BaseODS[Sequence[None], None]):
|
||||||
|
C_SATISFIED = 'Satisfaction of program restrictions'
|
||||||
|
EQUITY_ACCOUNTS = ['Equity', 'Income', 'Expenses']
|
||||||
|
NO_BALANCE = core.Balance()
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
balances: Balances,
|
balances: Balances,
|
||||||
*,
|
*,
|
||||||
|
@ -166,7 +195,7 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
one_day = datetime.timedelta(days=1)
|
one_day = datetime.timedelta(days=1)
|
||||||
date = balances.period_range.stop - one_day
|
date = balances.period_range.stop - one_day
|
||||||
self.period_name = date.strftime(date_fmt)
|
self.period_name = date.strftime(date_fmt)
|
||||||
date = balances.opening_range.stop - one_day
|
date = balances.prior_range.stop - one_day
|
||||||
self.opening_name = date.strftime(date_fmt)
|
self.opening_name = date.strftime(date_fmt)
|
||||||
|
|
||||||
def section_key(self, row: Sequence[None]) -> None:
|
def section_key(self, row: Sequence[None]) -> None:
|
||||||
|
@ -188,6 +217,7 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
|
|
||||||
def write_all(self) -> None:
|
def write_all(self) -> None:
|
||||||
self.write_financial_position()
|
self.write_financial_position()
|
||||||
|
self.write_activities()
|
||||||
|
|
||||||
def walk_classifications(self, cseq: Iterable[data.Account]) \
|
def walk_classifications(self, cseq: Iterable[data.Account]) \
|
||||||
-> Iterator[Tuple[str, Optional[data.Account]]]:
|
-> Iterator[Tuple[str, Optional[data.Account]]]:
|
||||||
|
@ -201,9 +231,14 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
last_prefix = parts
|
last_prefix = parts
|
||||||
yield f'{tabs}{tail}', classification
|
yield f'{tabs}{tail}', classification
|
||||||
|
|
||||||
def walk_classifications_by_account(self, account: str) \
|
def walk_classifications_by_account(
|
||||||
-> Iterator[Tuple[str, Optional[data.Account]]]:
|
self,
|
||||||
return self.walk_classifications(self.balances.classifications(account))
|
account: str,
|
||||||
|
sort_period: int=Period.PERIOD,
|
||||||
|
) -> Iterator[Tuple[str, Optional[data.Account]]]:
|
||||||
|
return self.walk_classifications(self.balances.classifications(
|
||||||
|
account, sort_period,
|
||||||
|
))
|
||||||
|
|
||||||
def write_financial_position(self) -> None:
|
def write_financial_position(self) -> None:
|
||||||
self.use_sheet("Financial Position")
|
self.use_sheet("Financial Position")
|
||||||
|
@ -227,15 +262,13 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
period_assets = core.MutableBalance()
|
period_assets = core.MutableBalance()
|
||||||
self.add_row(self.string_cell("Assets", stylename=self.style_bold))
|
self.add_row(self.string_cell("Assets", stylename=self.style_bold))
|
||||||
self.add_row()
|
self.add_row()
|
||||||
for text, classification in self.walk_classifications_by_account('Assets'):
|
for text, classification in self.walk_classifications_by_account('Assets', Period.ANY):
|
||||||
text_cell = self.string_cell(text)
|
text_cell = self.string_cell(text)
|
||||||
if classification is None:
|
if classification is None:
|
||||||
self.add_row(text_cell)
|
self.add_row(text_cell)
|
||||||
else:
|
else:
|
||||||
prior_bal = self.balances.total(
|
period_bal = self.balances.total(classification=classification)
|
||||||
classification=classification, period=Period.OPENING,
|
prior_bal = period_bal - self.balances.total(
|
||||||
)
|
|
||||||
period_bal = prior_bal + self.balances.total(
|
|
||||||
classification=classification, period=Period.PERIOD,
|
classification=classification, period=Period.PERIOD,
|
||||||
)
|
)
|
||||||
self.add_row(
|
self.add_row(
|
||||||
|
@ -261,15 +294,13 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
self.add_row()
|
self.add_row()
|
||||||
self.add_row(self.string_cell("Liabilities", stylename=self.style_bold))
|
self.add_row(self.string_cell("Liabilities", stylename=self.style_bold))
|
||||||
self.add_row()
|
self.add_row()
|
||||||
for text, classification in self.walk_classifications_by_account('Liabilities'):
|
for text, classification in self.walk_classifications_by_account('Liabilities', Period.ANY):
|
||||||
text_cell = self.string_cell(text)
|
text_cell = self.string_cell(text)
|
||||||
if classification is None:
|
if classification is None:
|
||||||
self.add_row(text_cell)
|
self.add_row(text_cell)
|
||||||
else:
|
else:
|
||||||
prior_bal = -self.balances.total(
|
period_bal = -self.balances.total(classification=classification)
|
||||||
classification=classification, period=Period.OPENING,
|
prior_bal = period_bal + self.balances.total(
|
||||||
)
|
|
||||||
period_bal = prior_bal - self.balances.total(
|
|
||||||
classification=classification, period=Period.PERIOD,
|
classification=classification, period=Period.PERIOD,
|
||||||
)
|
)
|
||||||
self.add_row(
|
self.add_row(
|
||||||
|
@ -291,16 +322,15 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
period_net = core.MutableBalance()
|
period_net = core.MutableBalance()
|
||||||
self.add_row(self.string_cell("Net Assets", stylename=self.style_bold))
|
self.add_row(self.string_cell("Net Assets", stylename=self.style_bold))
|
||||||
self.add_row()
|
self.add_row()
|
||||||
accounts = ['Equity', 'Income', 'Expenses']
|
|
||||||
for fund in [Fund.UNRESTRICTED, Fund.RESTRICTED]:
|
for fund in [Fund.UNRESTRICTED, Fund.RESTRICTED]:
|
||||||
preposition = "Without" if fund is Fund.UNRESTRICTED else "With"
|
preposition = "Without" if fund is Fund.UNRESTRICTED else "With"
|
||||||
prior_bal = -sum(
|
period_bal = -sum(
|
||||||
(self.balances.total(account=account, period=Period.OPENING, fund=fund)
|
(self.balances.total(account=account, fund=fund)
|
||||||
for account in accounts), core.MutableBalance(),
|
for account in self.EQUITY_ACCOUNTS), core.MutableBalance(),
|
||||||
)
|
)
|
||||||
period_bal = prior_bal - sum(
|
prior_bal = period_bal + sum(
|
||||||
(self.balances.total(account=account, period=Period.PERIOD, fund=fund)
|
(self.balances.total(account=account, fund=fund, period=Period.PERIOD)
|
||||||
for account in accounts), core.MutableBalance(),
|
for account in self.EQUITY_ACCOUNTS), core.MutableBalance(),
|
||||||
)
|
)
|
||||||
self.add_row(
|
self.add_row(
|
||||||
self.string_cell(f"{preposition} donor restrictions"),
|
self.string_cell(f"{preposition} donor restrictions"),
|
||||||
|
@ -323,6 +353,163 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
stylename=self.style_bottomline),
|
stylename=self.style_bottomline),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def write_activities(self) -> None:
|
||||||
|
self.use_sheet("Activities")
|
||||||
|
bal_kwargs: Sequence[Dict[str, Any]] = [
|
||||||
|
{'period': Period.PERIOD, 'fund': Fund.UNRESTRICTED},
|
||||||
|
{'period': Period.PERIOD, 'fund': Fund.RESTRICTED},
|
||||||
|
{'period': Period.PERIOD},
|
||||||
|
{'period': Period.PRIOR},
|
||||||
|
]
|
||||||
|
col_count = len(bal_kwargs) + 1
|
||||||
|
for index in range(col_count):
|
||||||
|
col_style = self.column_style(1.5 if index else 3)
|
||||||
|
self.sheet.addElement(odf.table.TableColumn(stylename=col_style))
|
||||||
|
self.add_row(
|
||||||
|
self.multiline_cell([
|
||||||
|
"DRAFT Statement of Activities",
|
||||||
|
self.period_name,
|
||||||
|
], numbercolumnsspanned=col_count, stylename=self.style_header)
|
||||||
|
)
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(
|
||||||
|
odf.table.TableCell(),
|
||||||
|
self.multiline_cell(["Without Donor", "Restrictions"],
|
||||||
|
stylename=self.style_huline),
|
||||||
|
self.multiline_cell(["With Donor", "Restrictions"],
|
||||||
|
stylename=self.style_huline),
|
||||||
|
self.multiline_cell(["Total Year Ended", self.period_name],
|
||||||
|
stylename=self.style_huline),
|
||||||
|
self.multiline_cell(["Total Year Ended", self.opening_name],
|
||||||
|
stylename=self.style_huline),
|
||||||
|
)
|
||||||
|
|
||||||
|
totals = [core.MutableBalance() for _ in bal_kwargs]
|
||||||
|
self.add_row(self.string_cell("Support and Revenue", stylename=self.style_bold))
|
||||||
|
self.add_row()
|
||||||
|
for text, classification in self.walk_classifications_by_account('Income'):
|
||||||
|
text_cell = self.string_cell(text)
|
||||||
|
if classification is None:
|
||||||
|
self.add_row(text_cell)
|
||||||
|
elif classification == self.C_SATISFIED:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
balances = [
|
||||||
|
-self.balances.total(classification=classification, **kwargs)
|
||||||
|
for kwargs in bal_kwargs
|
||||||
|
]
|
||||||
|
self.add_row(
|
||||||
|
text_cell,
|
||||||
|
*(self.balance_cell(bal) for bal in balances),
|
||||||
|
)
|
||||||
|
for total, bal in zip(totals, balances):
|
||||||
|
total += bal
|
||||||
|
self.add_row(
|
||||||
|
odf.table.TableCell(),
|
||||||
|
*(self.balance_cell(total, stylename=self.style_subtotline)
|
||||||
|
for total in totals),
|
||||||
|
)
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell("Net Assets released from restrictions:"),
|
||||||
|
)
|
||||||
|
released = self.balances.total(
|
||||||
|
account='Expenses', period=Period.PERIOD, fund=Fund.RESTRICTED,
|
||||||
|
) - self.balances.total(
|
||||||
|
classification=self.C_SATISFIED, period=Period.PERIOD, fund=Fund.RESTRICTED,
|
||||||
|
)
|
||||||
|
totals[0] += released
|
||||||
|
totals[1] -= released
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell(self.C_SATISFIED),
|
||||||
|
self.balance_cell(released),
|
||||||
|
self.balance_cell(-released),
|
||||||
|
self.balance_cell(self.NO_BALANCE),
|
||||||
|
self.balance_cell(self.NO_BALANCE),
|
||||||
|
)
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell("Total Support and Revenue"),
|
||||||
|
*(self.balance_cell(total, stylename=self.style_totline)
|
||||||
|
for total in totals),
|
||||||
|
)
|
||||||
|
|
||||||
|
period_expenses = core.MutableBalance()
|
||||||
|
prior_expenses = core.MutableBalance()
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(self.string_cell("Expenses", stylename=self.style_bold))
|
||||||
|
self.add_row()
|
||||||
|
for text, type_value in [
|
||||||
|
("Program services", 'program'),
|
||||||
|
("Management and administrative services", 'management'),
|
||||||
|
("Fundraising", 'fundraising'),
|
||||||
|
]:
|
||||||
|
period_bal = self.balances.total(
|
||||||
|
account='Expenses', period=Period.PERIOD, post_type=type_value,
|
||||||
|
)
|
||||||
|
prior_bal = self.balances.total(
|
||||||
|
account='Expenses', period=Period.PRIOR, post_type=type_value,
|
||||||
|
)
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell(text),
|
||||||
|
self.balance_cell(period_bal),
|
||||||
|
self.balance_cell(self.NO_BALANCE),
|
||||||
|
self.balance_cell(period_bal),
|
||||||
|
self.balance_cell(prior_bal),
|
||||||
|
)
|
||||||
|
period_expenses += period_bal
|
||||||
|
prior_expenses += prior_bal
|
||||||
|
period_bal = self.balances.total(account='Expenses', period=Period.PERIOD)
|
||||||
|
if (period_expenses - period_bal).clean_copy(1).is_zero():
|
||||||
|
period_bal = period_expenses
|
||||||
|
else:
|
||||||
|
logger.warning("Period functional expenses do not match total; math in columns B+D is wrong")
|
||||||
|
prior_bal = self.balances.total(account='Expenses', period=Period.PRIOR)
|
||||||
|
if (prior_expenses - prior_bal).clean_copy(1).is_zero():
|
||||||
|
prior_bal = prior_expenses
|
||||||
|
else:
|
||||||
|
logger.warning("Prior functional expenses do not match total; math in column E is wrong")
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell("Total Expenses"),
|
||||||
|
self.balance_cell(period_bal, stylename=self.style_totline),
|
||||||
|
self.balance_cell(self.NO_BALANCE, stylename=self.style_totline),
|
||||||
|
self.balance_cell(period_bal, stylename=self.style_totline),
|
||||||
|
self.balance_cell(prior_bal, stylename=self.style_totline),
|
||||||
|
)
|
||||||
|
|
||||||
|
totals[0] -= period_bal
|
||||||
|
totals[2] -= period_bal
|
||||||
|
totals[3] -= prior_bal
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell("Change in Net Assets"),
|
||||||
|
*(self.balance_cell(total) for total in totals),
|
||||||
|
)
|
||||||
|
|
||||||
|
for kwargs in bal_kwargs:
|
||||||
|
if kwargs['period'] is Period.PERIOD:
|
||||||
|
kwargs['period'] = Period.BEFORE_PERIOD
|
||||||
|
else:
|
||||||
|
kwargs['period'] = Period.OPENING
|
||||||
|
beginnings = [
|
||||||
|
-sum((self.balances.total(account=account, **kwargs)
|
||||||
|
for account in self.EQUITY_ACCOUNTS), core.MutableBalance())
|
||||||
|
for kwargs in bal_kwargs
|
||||||
|
]
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell("Beginning Net Assets"),
|
||||||
|
*(self.balance_cell(beg_bal) for beg_bal in beginnings),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.add_row()
|
||||||
|
self.add_row(
|
||||||
|
self.string_cell("Ending Net Assets"),
|
||||||
|
*(self.balance_cell(beg_bal + tot_bal, stylename=self.style_bottomline)
|
||||||
|
for beg_bal, tot_bal in zip(beginnings, totals)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
|
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(prog=PROGNAME)
|
parser = argparse.ArgumentParser(prog=PROGNAME)
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -5,7 +5,7 @@ from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name='conservancy_beancount',
|
name='conservancy_beancount',
|
||||||
description="Plugin, library, and reports for reading Conservancy's books",
|
description="Plugin, library, and reports for reading Conservancy's books",
|
||||||
version='1.7.2',
|
version='1.7.3',
|
||||||
author='Software Freedom Conservancy',
|
author='Software Freedom Conservancy',
|
||||||
author_email='info@sfconservancy.org',
|
author_email='info@sfconservancy.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
|
|
@ -83,7 +83,7 @@ def income_expense_balances():
|
||||||
({'classification': 'Postage'}, 30),
|
({'classification': 'Postage'}, 30),
|
||||||
({'classification': 'Services'}, 20),
|
({'classification': 'Services'}, 20),
|
||||||
({'classification': 'Nonexistent'}, None),
|
({'classification': 'Nonexistent'}, None),
|
||||||
({'period': Period.OPENING, 'account': 'Income'}, '-9.60'),
|
({'period': Period.PRIOR, 'account': 'Income'}, '-9.60'),
|
||||||
({'period': Period.PERIOD, 'account': 'Expenses'}, 26),
|
({'period': Period.PERIOD, 'account': 'Expenses'}, 26),
|
||||||
({'fund': Fund.RESTRICTED, 'account': 'Income'}, -10),
|
({'fund': Fund.RESTRICTED, 'account': 'Income'}, -10),
|
||||||
({'fund': Fund.UNRESTRICTED, 'account': 'Expenses'}, 25),
|
({'fund': Fund.UNRESTRICTED, 'account': 'Expenses'}, 25),
|
||||||
|
@ -91,10 +91,10 @@ def income_expense_balances():
|
||||||
({'post_type': 'fundraising'}, 20),
|
({'post_type': 'fundraising'}, 20),
|
||||||
({'post_type': 'management'}, 10),
|
({'post_type': 'management'}, 10),
|
||||||
({'post_type': 'Nonexistent'}, None),
|
({'post_type': 'Nonexistent'}, None),
|
||||||
({'period': Period.OPENING, 'post_type': 'RBI'}, '-4.80'),
|
({'period': Period.PRIOR, 'post_type': 'RBI'}, '-4.80'),
|
||||||
({'fund': Fund.RESTRICTED, 'post_type': 'program'}, 10),
|
({'fund': Fund.RESTRICTED, 'post_type': 'program'}, 10),
|
||||||
({'period': Period.PERIOD, 'fund': Fund.UNRESTRICTED, 'post_type': 'RBI'}, '-2.60'),
|
({'period': Period.PERIOD, 'fund': Fund.UNRESTRICTED, 'post_type': 'RBI'}, '-2.60'),
|
||||||
({'period': Period.OPENING, 'fund': Fund.RESTRICTED, 'post_type': 'program'}, '4.80'),
|
({'period': Period.PRIOR, 'fund': Fund.RESTRICTED, 'post_type': 'program'}, '4.80'),
|
||||||
({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_type': 'ø'}, None),
|
({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_type': 'ø'}, None),
|
||||||
])
|
])
|
||||||
def test_balance_total(income_expense_balances, kwargs, expected):
|
def test_balance_total(income_expense_balances, kwargs, expected):
|
||||||
|
|
Loading…
Reference in a new issue