From eaaf8fe98c1f93d141ef571d670f200b9e03fc5f 2020-08-17 19:28:08 From: Brett Smith Date: 2020-08-17 19:28:08 Subject: [PATCH] balance_sheet: Add functional expenses report. --- diff --git a/conservancy_beancount/reports/balance_sheet.py b/conservancy_beancount/reports/balance_sheet.py index 2aec62cc9f8f3418e2c0ce10bf2e9bb5e110b6e0..3146dd79242d9c801c706c227e2eca4bb20dc4e0 100644 --- a/conservancy_beancount/reports/balance_sheet.py +++ b/conservancy_beancount/reports/balance_sheet.py @@ -218,6 +218,7 @@ class Report(core.BaseODS[Sequence[None], None]): def write_all(self) -> None: self.write_financial_position() self.write_activities() + self.write_functional_expenses() def walk_classifications(self, cseq: Iterable[data.Account]) \ -> Iterator[Tuple[str, Optional[data.Account]]]: @@ -225,9 +226,9 @@ class Report(core.BaseODS[Sequence[None], None]): for classification in cseq: parts = classification.split(':') tail = parts.pop() - tabs = '\t' * len(parts) + tabs = ' ' * 4 * len(parts) if parts != last_prefix: - yield f'{tabs[1:]}{parts[-1]}', None + yield f'{tabs[4:]}{parts[-1]}', None last_prefix = parts yield f'{tabs}{tail}', classification @@ -509,6 +510,71 @@ class Report(core.BaseODS[Sequence[None], None]): for beg_bal, tot_bal in zip(beginnings, totals)), ) + def write_functional_expenses(self) -> None: + self.use_sheet("Functional Expenses") + bal_kwargs: Sequence[Dict[str, Any]] = [ + {'period': Period.PERIOD, 'post_type': 'program'}, + {'period': Period.PERIOD, 'post_type': 'management'}, + {'period': Period.PERIOD, 'post_type': 'fundraising'}, + {'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 Functional Expenses", + self.period_name, + ], numbercolumnsspanned=col_count, stylename=self.style_header) + ) + self.add_row() + self.add_row( + odf.table.TableCell(), + self.multiline_cell(["Program", "Services"], + stylename=self.style_huline), + self.multiline_cell(["Management and", "Administrative"], + stylename=self.style_huline), + self.multiline_cell(["Fundraising"], + 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), + ) + self.add_row() + + 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) - col_count, + ) + for total, bal in zip(totals, balances): + total += bal + self.add_row() + self.add_row( + self.string_cell("Total Expenses"), + *(self.balance_cell(tot_bal, stylename=self.style_bottomline) + for tot_bal in totals), + ) def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace: diff --git a/setup.py b/setup.py index 81b09dd053f8a2247ba8d91e5b81f31319d3cc28..e5cadd1cb09f3e292d50fa2f21028bfd25bc8f61 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.3', + version='1.7.4', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+',