diff --git a/conservancy_beancount/reports/balance_sheet.py b/conservancy_beancount/reports/balance_sheet.py index 3146dd79242d9c801c706c227e2eca4bb20dc4e0..4a405b41b79a46c346ec89a2c6f039ccbc4192bb 100644 --- a/conservancy_beancount/reports/balance_sheet.py +++ b/conservancy_beancount/reports/balance_sheet.py @@ -181,9 +181,11 @@ class Balances: class Report(core.BaseODS[Sequence[None], None]): + C_CASH = 'Cash' C_SATISFIED = 'Satisfaction of program restrictions' - EQUITY_ACCOUNTS = ['Equity', 'Income', 'Expenses'] + EQUITY_ACCOUNTS = frozenset(['Equity', 'Income', 'Expenses']) NO_BALANCE = core.Balance() + SPACE = ' ' * 4 def __init__(self, balances: Balances, @@ -219,6 +221,7 @@ class Report(core.BaseODS[Sequence[None], None]): self.write_financial_position() self.write_activities() self.write_functional_expenses() + self.write_cash_flows() def walk_classifications(self, cseq: Iterable[data.Account]) \ -> Iterator[Tuple[str, Optional[data.Account]]]: @@ -226,11 +229,11 @@ class Report(core.BaseODS[Sequence[None], None]): for classification in cseq: parts = classification.split(':') tail = parts.pop() - tabs = ' ' * 4 * len(parts) + space = self.SPACE * len(parts) if parts != last_prefix: - yield f'{tabs[4:]}{parts[-1]}', None + yield f'{space[len(self.SPACE):]}{parts[-1]}', None last_prefix = parts - yield f'{tabs}{tail}', classification + yield f'{space}{tail}', classification def walk_classifications_by_account( self, @@ -549,6 +552,7 @@ class Report(core.BaseODS[Sequence[None], None]): for text, classification in self.walk_classifications_by_account('Expenses'): text_cell = self.string_cell(text) if classification is None: + self.add_row(text_cell) if not text[0].isspace(): self.add_row() self.add_row(text_cell) @@ -576,6 +580,109 @@ class Report(core.BaseODS[Sequence[None], None]): for tot_bal in totals), ) + def write_cash_flows(self) -> None: + self.use_sheet("Cash Flows") + bal_kwargs: Sequence[Dict[str, Any]] = [ + {'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 Cash Flows", + self.period_name, + ], numbercolumnsspanned=col_count, stylename=self.style_header) + ) + self.add_row() + self.add_row( + odf.table.TableCell(), + self.string_cell(self.period_name, stylename=self.style_huline), + self.string_cell(self.opening_name, stylename=self.style_huline), + ) + self.add_row() + self.add_row(self.string_cell( + "Cash Flows from Operating Activities", + stylename=self.style_bold, + )) + self.add_row() + + totals = [ + -sum((self.balances.total(account=account, **kwargs) + for account in self.EQUITY_ACCOUNTS), core.MutableBalance()) + for kwargs in bal_kwargs + ] + self.add_row( + self.string_cell("Change in Net Assets"), + *(self.balance_cell(bal) for bal in 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 + 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 + self.add_row( + self.string_cell("Net cash provided by operating activites"), + *(self.balance_cell(tot_bal, stylename=self.style_totline) + for tot_bal in totals), + ) + self.add_row() + + self.add_row( + self.string_cell("Net Increase in Cash"), + *(self.balance_cell(tot_bal) for tot_bal in totals), + ) + self.add_row() + balances = [ + self.balances.total(classification=self.C_CASH, period=period) + for period in [Period.BEFORE_PERIOD, Period.OPENING] + ] + self.add_row( + self.string_cell("Beginning Cash"), + *(self.balance_cell(bal) for bal in balances), + ) + self.add_row() + self.add_row( + self.string_cell("Ending Cash"), + *(self.balance_cell(tot + bal, stylename=self.style_bottomline) + for tot, bal in zip(totals, balances)), + ) + def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace: parser = argparse.ArgumentParser(prog=PROGNAME) diff --git a/setup.py b/setup.py index e5cadd1cb09f3e292d50fa2f21028bfd25bc8f61..867d9d5125c71e05a1c13d07d0c96026556ef554 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='conservancy_beancount', description="Plugin, library, and reports for reading Conservancy's books", - version='1.7.4', + version='1.8.0', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+',