fund: Add bottom line totals to Fund Report. RT#4582.

This required keeping the balances from write_row, and then a lot of other
changes followed from that. In particular it makes more sense to build the
fund report sheet from scratch rather than copying the breakdowns report and
chiseling the fund report out of it.
This commit is contained in:
Brett Smith 2020-07-01 15:56:39 -04:00
parent 7a0fa4fb57
commit 7702a1f03c
2 changed files with 80 additions and 67 deletions

View file

@ -61,6 +61,7 @@ from typing import (
Sequence, Sequence,
TextIO, TextIO,
Tuple, Tuple,
Union,
) )
from ..beancount_types import ( from ..beancount_types import (
MetaValue, MetaValue,
@ -98,32 +99,48 @@ class ODSReport(core.BaseODS[FundPosts, None]):
super().__init__() super().__init__()
self.start_date = start_date self.start_date = start_date
self.stop_date = stop_date self.stop_date = stop_date
self.unrestricted: AccountsMap = {}
def section_key(self, row: FundPosts) -> None: def section_key(self, row: FundPosts) -> None:
return None return None
def start_spreadsheet(self) -> None: def start_spreadsheet(self, *, expanded: bool=True) -> None:
self.use_sheet("With Breakdowns") headers = [["Fund"], ["Balance as of", self.start_date.isoformat()]]
for width in [2.5, 1.5, 1.2, 1.2, 1.2, 1.5, 1.2, 1.3, 1.2, 1.3]: 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) col_style = self.column_style(width)
self.sheet.addElement(odf.table.TableColumn(stylename=col_style)) self.sheet.addElement(odf.table.TableColumn(stylename=col_style))
center_bold = self.merge_styles(self.style_centertext, self.style_bold) center_bold = self.merge_styles(self.style_centertext, self.style_bold)
self.add_row( row = self.add_row(*(
self.string_cell( self.multiline_cell(header, stylename=center_bold)
"Fund", stylename=self.merge_styles(self.style_endtext, self.style_bold), for header in headers
), ))
self.multiline_cell(["Balance as of", self.start_date.isoformat()], row.firstChild.setAttribute(
stylename=center_bold), 'stylename', self.merge_styles(self.style_endtext, self.style_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),
) )
self.lock_first_row() self.lock_first_row()
self.lock_first_column() self.lock_first_column()
@ -136,45 +153,25 @@ class ODSReport(core.BaseODS[FundPosts, None]):
self.add_row() self.add_row()
def end_spreadsheet(self) -> None: def end_spreadsheet(self) -> None:
sheet = self.copy_element(self.sheet) start_sheet = 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)
self.set_open_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]: def _row_balances(self, accounts_map: AccountsMap) -> Iterable[core.Balance]:
acct_order = ['Income', 'Expenses', 'Equity'] acct_order = ['Income', 'Expenses', 'Equity']
@ -196,22 +193,32 @@ class ODSReport(core.BaseODS[FundPosts, None]):
pass pass
yield core.normalize_amount_func(info_key)(balance) yield core.normalize_amount_func(info_key)(balance)
def write_row(self, row: FundPosts) -> None: def write_balances(self,
fund, accounts_map = row fund: str,
if fund == UNRESTRICTED_FUND: balances: Iterable[core.Balance],
assert not self.unrestricted style: Union[None, str, odf.style.Style]=None,
self.unrestricted = accounts_map ) -> odf.table.TableRow:
return return self.add_row(
self.add_row(
self.string_cell(fund, stylename=self.style_endtext), 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: def write(self, rows: Iterable[FundPosts]) -> None:
self.balances: Dict[str, Sequence[core.Balance]] = collections.OrderedDict()
super().write(rows) super().write(rows)
if self.unrestricted: try:
unrestricted = self.balances[UNRESTRICTED_FUND]
except KeyError:
pass
else:
self.add_row() self.add_row()
self.write_row(("Unrestricted", self.unrestricted)) self.write_balances("Unrestricted", unrestricted)
class TextReport: class TextReport:

View file

@ -172,6 +172,12 @@ def check_ods_sheet(sheet, account_balances, *, full):
for key, balances in account_balances.items() for key, balances in account_balances.items()
if key != 'Conservancy' and any(v >= .5 for v in balances.values()) 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): for row in itertools.islice(sheet.getElementsByType(odf.table.TableRow), 4, None):
cells = iter(testutil.ODSCell.from_row(row)) cells = iter(testutil.ODSCell.from_row(row))
try: try: