balance_sheet: Refactor out Report.write_classifications_by_account.
This commit is contained in:
parent
90ae343a1a
commit
b7dee6a88a
1 changed files with 89 additions and 162 deletions
|
@ -19,6 +19,7 @@ import collections
|
|||
import datetime
|
||||
import enum
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
@ -27,6 +28,7 @@ from pathlib import Path
|
|||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Collection,
|
||||
Dict,
|
||||
Hashable,
|
||||
|
@ -57,6 +59,8 @@ EQUITY_ACCOUNTS = frozenset(['Equity', 'Income', 'Expenses'])
|
|||
PROGNAME = 'balance-sheet-report'
|
||||
logger = logging.getLogger('conservancy_beancount.tools.balance_sheet')
|
||||
|
||||
KWArgs = Mapping[str, Any]
|
||||
|
||||
class Fund(enum.IntFlag):
|
||||
RESTRICTED = enum.auto()
|
||||
UNRESTRICTED = enum.auto()
|
||||
|
@ -278,98 +282,83 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
self.add_row()
|
||||
self.add_row(*header_cells)
|
||||
|
||||
def write_classifications_by_account(
|
||||
self,
|
||||
account: str,
|
||||
balance_kwargs: Sequence[KWArgs],
|
||||
exclude_classifications: Collection[str]=frozenset(),
|
||||
text_prefix: str='',
|
||||
norm_func: Optional[Callable[[core.Balance], core.Balance]]=None,
|
||||
) -> Sequence[core.Balance]:
|
||||
if norm_func is None:
|
||||
norm_func = core.normalize_amount_func(f'{account}:RootsOK')
|
||||
assert len(balance_kwargs) + 1 == self.col_count, \
|
||||
"called write_classifications with wrong number of balance_kwargs"
|
||||
retval = [core.MutableBalance() for _ in balance_kwargs]
|
||||
for text, classification in self.walk_classifications_by_account(account):
|
||||
text_cell = self.string_cell(text_prefix + text)
|
||||
if classification is None:
|
||||
if not text[0].isspace():
|
||||
self.add_row()
|
||||
self.add_row(text_cell)
|
||||
elif classification in exclude_classifications:
|
||||
pass
|
||||
else:
|
||||
row = self.add_row(text_cell)
|
||||
for kwargs, total_bal in zip(balance_kwargs, retval):
|
||||
balance = norm_func(self.balances.total(
|
||||
classification=classification, **kwargs,
|
||||
))
|
||||
row.addElement(self.balance_cell(balance))
|
||||
total_bal += balance
|
||||
return retval
|
||||
|
||||
def write_financial_position(self) -> None:
|
||||
self.start_sheet("Financial Position")
|
||||
balance_kwargs: Sequence[KWArgs] = [
|
||||
{'period': Period.ANY},
|
||||
{'period': Period.BEFORE_PERIOD},
|
||||
]
|
||||
|
||||
prior_assets = core.MutableBalance()
|
||||
period_assets = core.MutableBalance()
|
||||
self.add_row(self.string_cell("Assets", stylename=self.style_bold))
|
||||
self.add_row()
|
||||
for text, classification in self.walk_classifications_by_account('Assets'):
|
||||
text_cell = self.string_cell(text)
|
||||
if classification is None:
|
||||
self.add_row(text_cell)
|
||||
else:
|
||||
period_bal = self.balances.total(classification=classification)
|
||||
prior_bal = period_bal - self.balances.total(
|
||||
classification=classification, period=Period.PERIOD,
|
||||
)
|
||||
self.add_row(
|
||||
text_cell,
|
||||
self.balance_cell(period_bal),
|
||||
self.balance_cell(prior_bal),
|
||||
)
|
||||
prior_assets += prior_bal
|
||||
period_assets += period_bal
|
||||
asset_totals = self.write_classifications_by_account('Assets', balance_kwargs)
|
||||
self.add_row()
|
||||
self.add_row(
|
||||
self.string_cell("Total Assets"),
|
||||
self.balance_cell(period_assets, stylename=self.style_bottomline),
|
||||
self.balance_cell(prior_assets, stylename=self.style_bottomline),
|
||||
*(self.balance_cell(balance, stylename=self.style_bottomline)
|
||||
for balance in asset_totals),
|
||||
)
|
||||
self.add_row()
|
||||
self.add_row()
|
||||
|
||||
prior_liabilities = core.MutableBalance()
|
||||
period_liabilities = core.MutableBalance()
|
||||
self.add_row(self.string_cell("Liabilities and Net Assets",
|
||||
stylename=self.style_bold))
|
||||
self.add_row()
|
||||
self.add_row(self.string_cell("Liabilities", stylename=self.style_bold))
|
||||
self.add_row()
|
||||
for text, classification in self.walk_classifications_by_account('Liabilities'):
|
||||
text_cell = self.string_cell(text)
|
||||
if classification is None:
|
||||
self.add_row(text_cell)
|
||||
else:
|
||||
period_bal = -self.balances.total(classification=classification)
|
||||
prior_bal = period_bal + self.balances.total(
|
||||
classification=classification, period=Period.PERIOD,
|
||||
)
|
||||
self.add_row(
|
||||
text_cell,
|
||||
self.balance_cell(period_bal),
|
||||
self.balance_cell(prior_bal),
|
||||
)
|
||||
prior_liabilities += prior_bal
|
||||
period_liabilities += period_bal
|
||||
liabilities = self.write_classifications_by_account('Liabilities', balance_kwargs)
|
||||
self.add_row(
|
||||
self.string_cell("Total Liabilities"),
|
||||
self.balance_cell(period_liabilities, stylename=self.style_totline),
|
||||
self.balance_cell(prior_liabilities, stylename=self.style_totline),
|
||||
*(self.balance_cell(balance, stylename=self.style_totline)
|
||||
for balance in liabilities),
|
||||
)
|
||||
self.add_row()
|
||||
self.add_row()
|
||||
|
||||
prior_net = core.MutableBalance()
|
||||
period_net = core.MutableBalance()
|
||||
equity_totals = [core.MutableBalance() for _ in balance_kwargs]
|
||||
self.add_row(self.string_cell("Net Assets", stylename=self.style_bold))
|
||||
self.add_row()
|
||||
for fund in [Fund.UNRESTRICTED, Fund.RESTRICTED]:
|
||||
preposition = "Without" if fund is Fund.UNRESTRICTED else "With"
|
||||
period_bal = -self.balances.total(account=EQUITY_ACCOUNTS, fund=fund)
|
||||
prior_bal = period_bal + self.balances.total(
|
||||
account=EQUITY_ACCOUNTS, fund=fund, period=Period.PERIOD,
|
||||
)
|
||||
self.add_row(
|
||||
self.string_cell(f"{preposition} donor restrictions"),
|
||||
self.balance_cell(period_bal),
|
||||
self.balance_cell(prior_bal),
|
||||
)
|
||||
prior_net += prior_bal
|
||||
period_net += period_bal
|
||||
row = self.add_row(self.string_cell(f"{preposition} donor restrictions"))
|
||||
for kwargs, total_bal in zip(balance_kwargs, equity_totals):
|
||||
balance = -self.balances.total(account=EQUITY_ACCOUNTS, fund=fund, **kwargs)
|
||||
row.addElement(self.balance_cell(balance))
|
||||
total_bal += balance
|
||||
self.add_row(
|
||||
self.string_cell("Total Net Assets"),
|
||||
self.balance_cell(period_net, stylename=self.style_subtotline),
|
||||
self.balance_cell(prior_net, stylename=self.style_subtotline),
|
||||
*(self.balance_cell(balance, stylename=self.style_subtotline)
|
||||
for balance in equity_totals),
|
||||
)
|
||||
self.add_row()
|
||||
self.add_row(
|
||||
self.string_cell("Total Liabilities and Net Assets"),
|
||||
self.balance_cell(period_liabilities + period_net,
|
||||
stylename=self.style_bottomline),
|
||||
self.balance_cell(prior_liabilities + prior_net,
|
||||
stylename=self.style_bottomline),
|
||||
*(self.balance_cell(ltot + etot, stylename=self.style_bottomline)
|
||||
for ltot, etot in zip(liabilities, equity_totals)),
|
||||
)
|
||||
|
||||
def write_activities(self) -> None:
|
||||
|
@ -386,30 +375,15 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
{'period': Period.PRIOR},
|
||||
]
|
||||
|
||||
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
|
||||
income_totals = self.write_classifications_by_account(
|
||||
'Income', bal_kwargs, (self.C_SATISFIED,),
|
||||
)
|
||||
self.add_row(
|
||||
odf.table.TableCell(),
|
||||
*(self.balance_cell(total, stylename=self.style_subtotline)
|
||||
for total in totals),
|
||||
for total in income_totals),
|
||||
)
|
||||
self.add_row()
|
||||
self.add_row(
|
||||
|
@ -420,20 +394,18 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
) - self.balances.total(
|
||||
classification=self.C_SATISFIED, period=Period.PERIOD, fund=Fund.RESTRICTED,
|
||||
)
|
||||
totals[0] += released
|
||||
totals[1] -= released
|
||||
other_totals = [core.MutableBalance() for _ in bal_kwargs]
|
||||
other_totals[0] += released
|
||||
other_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.balance_cell(bal) for bal in other_totals),
|
||||
)
|
||||
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),
|
||||
*(self.balance_cell(inctot + otot, stylename=self.style_totline)
|
||||
for inctot, otot in zip(income_totals, other_totals)),
|
||||
)
|
||||
|
||||
period_expenses = core.MutableBalance()
|
||||
|
@ -479,13 +451,14 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
self.balance_cell(prior_bal, stylename=self.style_totline),
|
||||
)
|
||||
|
||||
totals[0] -= period_bal
|
||||
totals[2] -= period_bal
|
||||
totals[3] -= prior_bal
|
||||
other_totals[0] -= period_bal
|
||||
other_totals[2] -= period_bal
|
||||
other_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),
|
||||
*(self.balance_cell(inctot + otot)
|
||||
for inctot, otot in zip(income_totals, other_totals)),
|
||||
)
|
||||
|
||||
for kwargs in bal_kwargs:
|
||||
|
@ -493,21 +466,21 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
kwargs['period'] = Period.BEFORE_PERIOD
|
||||
else:
|
||||
kwargs['period'] = Period.OPENING
|
||||
beginnings = [
|
||||
equity_totals = [
|
||||
-self.balances.total(account=EQUITY_ACCOUNTS, **kwargs)
|
||||
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.balance_cell(beg_bal) for beg_bal in equity_totals),
|
||||
)
|
||||
|
||||
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)),
|
||||
*(self.balance_cell(inctot + otot + eqtot, stylename=self.style_bottomline)
|
||||
for inctot, otot, eqtot in zip(income_totals, other_totals, equity_totals)),
|
||||
)
|
||||
|
||||
def write_functional_expenses(self) -> None:
|
||||
|
@ -518,38 +491,13 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
["Fundraising"],
|
||||
totals_prefix=["Total Year Ended"],
|
||||
)
|
||||
bal_kwargs: Sequence[Dict[str, Any]] = [
|
||||
totals = self.write_classifications_by_account('Expenses', [
|
||||
{'period': Period.PERIOD, 'post_type': 'program'},
|
||||
{'period': Period.PERIOD, 'post_type': 'management'},
|
||||
{'period': Period.PERIOD, 'post_type': 'fundraising'},
|
||||
{'period': Period.PERIOD},
|
||||
{'period': Period.PRIOR},
|
||||
]
|
||||
|
||||
totals = [core.MutableBalance() for _ in bal_kwargs]
|
||||
for text, classification in self.walk_classifications_by_account('Expenses'):
|
||||
text_cell = self.string_cell(text)
|
||||
if classification is None:
|
||||
if not text[0].isspace():
|
||||
self.add_row()
|
||||
self.add_row(text_cell)
|
||||
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),
|
||||
)
|
||||
break_bal = sum(balances[:3], core.MutableBalance())
|
||||
if not (break_bal - balances[3]).clean_copy(1).is_zero():
|
||||
logger.warning(
|
||||
"Functional expenses breakdown does not match total on row %s",
|
||||
len(self.sheet.childNodes) - self.col_count,
|
||||
)
|
||||
for total, bal in zip(totals, balances):
|
||||
total += bal
|
||||
])
|
||||
self.add_row()
|
||||
self.add_row(
|
||||
self.string_cell("Total Expenses"),
|
||||
|
@ -563,6 +511,7 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
{'period': Period.PERIOD},
|
||||
{'period': Period.PRIOR},
|
||||
]
|
||||
norm_func = operator.neg
|
||||
|
||||
self.add_row(self.string_cell(
|
||||
"Cash Flows from Operating Activities",
|
||||
|
@ -570,52 +519,30 @@ class Report(core.BaseODS[Sequence[None], None]):
|
|||
))
|
||||
self.add_row()
|
||||
|
||||
totals = [
|
||||
equity_totals = [
|
||||
-self.balances.total(account=EQUITY_ACCOUNTS, **kwargs)
|
||||
for kwargs in bal_kwargs
|
||||
]
|
||||
self.add_row(
|
||||
self.string_cell("Change in Net Assets"),
|
||||
*(self.balance_cell(bal) for bal in totals),
|
||||
*(self.balance_cell(bal) for bal in equity_totals),
|
||||
)
|
||||
self.add_row(self.string_cell(
|
||||
"(Increase) decrease in operating assets:",
|
||||
))
|
||||
for text, classification in self.walk_classifications_by_account('Assets'):
|
||||
text_cell = self.string_cell(self.SPACE + text)
|
||||
if classification is None:
|
||||
self.add_row(text_cell)
|
||||
elif classification == self.C_CASH:
|
||||
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
|
||||
asset_totals = self.write_classifications_by_account(
|
||||
'Assets', bal_kwargs, (self.C_CASH,), self.SPACE, norm_func,
|
||||
)
|
||||
self.add_row(self.string_cell(
|
||||
"Increase (decrease) in operating liabilities:",
|
||||
))
|
||||
for text, classification in self.walk_classifications_by_account('Liabilities'):
|
||||
text_cell = self.string_cell(self.SPACE + text)
|
||||
if classification is None:
|
||||
self.add_row(text_cell)
|
||||
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
|
||||
liabilities = self.write_classifications_by_account(
|
||||
'Liabilities', bal_kwargs, (), self.SPACE, norm_func,
|
||||
)
|
||||
totals = [
|
||||
sum(bals, core.MutableBalance())
|
||||
for bals in zip(equity_totals, asset_totals, liabilities)
|
||||
]
|
||||
self.add_row(
|
||||
self.string_cell("Net cash provided by operating activites"),
|
||||
*(self.balance_cell(tot_bal, stylename=self.style_totline)
|
||||
|
|
Loading…
Reference in a new issue