From b7dee6a88a81a04fb49cd394f11876b24d5f6065 2020-08-18 07:27:27 From: Brett Smith Date: 2020-08-18 07:27:27 Subject: [PATCH] balance_sheet: Refactor out Report.write_classifications_by_account. --- diff --git a/conservancy_beancount/reports/balance_sheet.py b/conservancy_beancount/reports/balance_sheet.py index 92f540ac96bb91157c7e5b91ade08d4956cff552..bd811f6bd2cd4c4614e9419573f424a04ccd2b9e 100644 --- a/conservancy_beancount/reports/balance_sheet.py +++ b/conservancy_beancount/reports/balance_sheet.py @@ -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_financial_position(self) -> None: - self.start_sheet("Financial Position") - - 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) + 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: - 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 + 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}, + ] + + 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)