diff --git a/conservancy_beancount/reports/fund.py b/conservancy_beancount/reports/fund.py index a789868e3535c152de28696253b63746d29f1a93..ae2bb4163f76632fd4c44175e22ad1508013b8b7 100644 --- a/conservancy_beancount/reports/fund.py +++ b/conservancy_beancount/reports/fund.py @@ -61,6 +61,7 @@ from typing import ( Sequence, TextIO, Tuple, + Union, ) from ..beancount_types import ( MetaValue, @@ -98,32 +99,48 @@ class ODSReport(core.BaseODS[FundPosts, None]): super().__init__() self.start_date = start_date self.stop_date = stop_date - self.unrestricted: AccountsMap = {} def section_key(self, row: FundPosts) -> None: return None - def start_spreadsheet(self) -> None: - self.use_sheet("With Breakdowns") - for width in [2.5, 1.5, 1.2, 1.2, 1.2, 1.5, 1.2, 1.3, 1.2, 1.3]: + def start_spreadsheet(self, *, expanded: bool=True) -> None: + headers = [["Fund"], ["Balance as of", self.start_date.isoformat()]] + if expanded: + sheet_name = "With Breakdowns" + headers += [["Income"], ["Expenses"], ["Equity"]] + else: + sheet_name = "Fund Report" + headers += [["Additions"], ["Releases from", "Restrictions"]] + headers.append(["Balance as of", self.stop_date.isoformat()]) + if expanded: + headers += [ + ["Of which", "Receivable"], + ["Of which", "Prepaid Expenses"], + ["Of which", "Payable"], + ["Of which", "Unearned Income"], + ] + + self.use_sheet(sheet_name) + for header in headers: + first_line = header[0] + if first_line == 'Fund': + width = 2.0 + elif first_line == 'Balance as of': + width = 1.5 + elif first_line == 'Of which': + width = 1.3 + else: + width = 1.2 col_style = self.column_style(width) self.sheet.addElement(odf.table.TableColumn(stylename=col_style)) + center_bold = self.merge_styles(self.style_centertext, self.style_bold) - self.add_row( - self.string_cell( - "Fund", stylename=self.merge_styles(self.style_endtext, self.style_bold), - ), - self.multiline_cell(["Balance as of", self.start_date.isoformat()], - stylename=center_bold), - self.string_cell("Income", stylename=center_bold), - self.string_cell("Expenses", stylename=center_bold), - self.string_cell("Equity", stylename=center_bold), - self.multiline_cell(["Balance as of", self.stop_date.isoformat()], - stylename=center_bold), - self.multiline_cell(["Of Which", "Receivable"], stylename=center_bold), - self.multiline_cell(["Of Which", "Prepaid Expenses"], stylename=center_bold), - self.multiline_cell(["Of Which", "Payable"], stylename=center_bold), - self.multiline_cell(["Of Which", "Unearned Income"], stylename=center_bold), + row = self.add_row(*( + self.multiline_cell(header, stylename=center_bold) + for header in headers + )) + row.firstChild.setAttribute( + 'stylename', self.merge_styles(self.style_endtext, self.style_bold), ) self.lock_first_row() self.lock_first_column() @@ -136,45 +153,25 @@ class ODSReport(core.BaseODS[FundPosts, None]): self.add_row() def end_spreadsheet(self) -> None: - sheet = self.copy_element(self.sheet) - sheet.setAttribute('name', 'Fund Report') - row_qname = odf.table.TableRow().qname - skip_rows: List[int] = [] - report_threshold = Decimal('.5') - first_row = True - for index, row in enumerate(sheet.childNodes): - if len(row.childNodes) < 6: - continue - row.childNodes = [*row.childNodes[:4], row.childNodes[5]] - if row.qname != row_qname: - pass - elif first_row: - ref_child = row.childNodes[2] - stylename = ref_child.getAttribute('stylename') - row.insertBefore(self.string_cell( - "Additions", stylename=stylename, - ), ref_child) - row.insertBefore(self.multiline_cell( - ["Releases from", "Restrictions"], stylename=stylename, - ), ref_child) - del row.childNodes[4:6] - first_row = False - # Filter out fund rows that don't have anything reportable. - elif not any( - # Multiple childNodes means it's a multi-currency balance. - len(cell.childNodes) > 1 - # Some column has to round up to 1 to be reportable. - or (cell.getAttribute('valuetype') == 'currency' - and Decimal(cell.getAttribute('value')) >= report_threshold) - for cell in row.childNodes - ): - skip_rows.append(index) - for index in reversed(skip_rows): - del sheet.childNodes[index] - self.lock_first_row(sheet) - self.lock_first_column(sheet) - self.document.spreadsheet.insertBefore(sheet, self.sheet) + start_sheet = self.sheet self.set_open_sheet(self.sheet) + self.start_spreadsheet(expanded=False) + bal_indexes = [0, 1, 2, 4] + totals = [core.MutableBalance() for _ in bal_indexes] + threshold = Decimal('.5') + for fund, balances in self.balances.items(): + balances = [balances[index] for index in bal_indexes] + if (not all(bal.clean_copy(threshold).le_zero() for bal in balances) + and fund != UNRESTRICTED_FUND): + self.write_balances(fund, balances) + for total, bal in zip(totals, balances): + total += bal + self.write_balances('', totals, self.merge_styles( + self.border_style(core.Border.TOP, '.75pt'), + self.border_style(core.Border.BOTTOM, '1.5pt', 'double'), + )) + self.document.spreadsheet.childNodes.reverse() + self.sheet = start_sheet def _row_balances(self, accounts_map: AccountsMap) -> Iterable[core.Balance]: acct_order = ['Income', 'Expenses', 'Equity'] @@ -196,22 +193,32 @@ class ODSReport(core.BaseODS[FundPosts, None]): pass yield core.normalize_amount_func(info_key)(balance) - def write_row(self, row: FundPosts) -> None: - fund, accounts_map = row - if fund == UNRESTRICTED_FUND: - assert not self.unrestricted - self.unrestricted = accounts_map - return - self.add_row( + def write_balances(self, + fund: str, + balances: Iterable[core.Balance], + style: Union[None, str, odf.style.Style]=None, + ) -> odf.table.TableRow: + return self.add_row( self.string_cell(fund, stylename=self.style_endtext), - *(self.balance_cell(bal) for bal in self._row_balances(accounts_map)), + *(self.balance_cell(bal, stylename=style) for bal in balances), ) + def write_row(self, row: FundPosts) -> None: + fund, accounts_map = row + self.balances[fund] = list(self._row_balances(accounts_map)) + if fund != UNRESTRICTED_FUND: + self.write_balances(fund, self.balances[fund]) + def write(self, rows: Iterable[FundPosts]) -> None: + self.balances: Dict[str, Sequence[core.Balance]] = collections.OrderedDict() super().write(rows) - if self.unrestricted: + try: + unrestricted = self.balances[UNRESTRICTED_FUND] + except KeyError: + pass + else: self.add_row() - self.write_row(("Unrestricted", self.unrestricted)) + self.write_balances("Unrestricted", unrestricted) class TextReport: diff --git a/tests/test_reports_fund.py b/tests/test_reports_fund.py index 61d7563f62f3eb8fafc629943b1a60f14fef4207..677266a903af76794605afbcda034cf2f08793af 100644 --- a/tests/test_reports_fund.py +++ b/tests/test_reports_fund.py @@ -172,6 +172,12 @@ def check_ods_sheet(sheet, account_balances, *, full): for key, balances in account_balances.items() if key != 'Conservancy' and any(v >= .5 for v in balances.values()) } + totals = {key: Decimal() for key in + ['opening', 'Income', 'Expenses', 'Equity:Realized']} + for fund, balances in account_bals.items(): + for key in totals: + totals[key] += balances[key] + account_bals[''] = totals for row in itertools.islice(sheet.getElementsByType(odf.table.TableRow), 4, None): cells = iter(testutil.ODSCell.from_row(row)) try: