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:
parent
7a0fa4fb57
commit
7702a1f03c
2 changed files with 80 additions and 67 deletions
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue