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,
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:

View file

@ -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: