From 7441f4ef0ce1448f751f0754c22dac53d4a5d2b9 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Wed, 17 Jun 2020 18:25:47 -0400 Subject: [PATCH] ledger: Correct period totals. RT#11661. The period totals were reporting the balance of all the loaded postings, not just the ones in the reporting date range. Like the accrual report, introduce a RelatedPostings subclass that records and saves all the information we need at group definition time, to help us get it consistently right rather than redoing the same math over and over. --- conservancy_beancount/reports/ledger.py | 51 +++++++++++++++++-------- setup.py | 2 +- tests/test_reports_ledger.py | 3 +- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/conservancy_beancount/reports/ledger.py b/conservancy_beancount/reports/ledger.py index b69c180..e28c098 100644 --- a/conservancy_beancount/reports/ledger.py +++ b/conservancy_beancount/reports/ledger.py @@ -69,6 +69,7 @@ from pathlib import Path import odf.table # type:ignore[import] +from beancount.core import data as bc_data from beancount.parser import printer as bc_printer from . import core @@ -84,6 +85,20 @@ PostTally = List[Tuple[int, data.Account]] PROGNAME = 'ledger-report' logger = logging.getLogger('conservancy_beancount.reports.ledger') +class AccountPostings(core.RelatedPostings): + START_DATE: datetime.date + + def __init__(self, + source: Iterable[data.Posting]=(), + *, + _can_own: bool=False, + ) -> None: + super().__init__(source, _can_own=_can_own) + self.start_bal = self.balance_at_cost_by_date(self.START_DATE) + self.stop_bal = self.balance_at_cost() + self.period_bal = self.stop_bal - self.start_bal + + class LedgerODS(core.BaseODS[data.Posting, data.Account]): CORE_COLUMNS: Sequence[str] = [ 'Date', @@ -268,17 +283,21 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]): def _report_section_balance(self, key: data.Account, date_key: str) -> None: uses_opening = key.is_under('Assets', 'Equity', 'Liabilities') + related = self.account_groups[key] if date_key == 'start': if not uses_opening: return date = self.date_range.start + balance = related.start_bal description = "Opening Balance" else: date = self.date_range.stop - description = "Ending Balance" if uses_opening else "Period Total" - balance = self.norm_func( - self.account_groups[key].balance_at_cost_by_date(date) - ) + if uses_opening: + balance = related.stop_bal + description = "Ending Balance" + else: + balance = related.period_bal + description = "Period Total" self.add_row( self.date_cell(date, stylename=self.merge_styles( self.style_bold, self.style_date, @@ -286,7 +305,7 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]): odf.table.TableCell(), self.string_cell(description, stylename=self.style_bold), odf.table.TableCell(), - self.balance_cell(balance, stylename=self.style_bold), + self.balance_cell(self.norm_func(balance), stylename=self.style_bold), ) def start_section(self, key: data.Account) -> None: @@ -327,11 +346,13 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]): ) def _combined_balance_row(self, - date: datetime.date, balance_accounts: Sequence[str], + attr_name: str, ) -> None: + date = getattr(self.date_range, attr_name) + balance_attrname = f'{attr_name}_bal' balance = -sum(( - related.balance_at_cost_by_date(date) + getattr(related, balance_attrname) for account, related in self.account_groups.items() if account.is_under(*balance_accounts) ), core.MutableBalance()) @@ -365,23 +386,23 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]): numbercolumnsspanned=2, )) self.add_row() - self._combined_balance_row(self.date_range.start, balance_accounts) + self._combined_balance_row(balance_accounts, 'start') for _, account in self._sort_and_filter_accounts( self.account_groups, balance_accounts, ): - related = self.account_groups[account] - # start_bal - stop_bal == -(stop_bal - start_bal) - balance = related.balance_at_cost_by_date(self.date_range.start) - balance -= related.balance_at_cost_by_date(self.date_range.stop) + balance = self.account_groups[account].period_bal if not balance.is_zero(): self.add_row( self.string_cell(account, stylename=self.style_endtext), - self.balance_cell(balance), + self.balance_cell(-balance), ) - self._combined_balance_row(self.date_range.stop, balance_accounts) + self._combined_balance_row(balance_accounts, 'stop') def write(self, rows: Iterable[data.Posting]) -> None: - self.account_groups = dict(core.RelatedPostings.group_by_account(rows)) + AccountPostings.START_DATE = self.date_range.start + self.account_groups = dict(AccountPostings.group_by_account( + post for post in rows if post.meta.date < self.date_range.stop + )) self.write_balance_sheet() tally_by_account_iter = ( (account, sum(1 for post in related if post.meta.date in self.date_range)) diff --git a/setup.py b/setup.py index 385fa5b..911a3da 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.2.5', + version='1.2.6', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+', diff --git a/tests/test_reports_ledger.py b/tests/test_reports_ledger.py index 00df81a..db65abd 100644 --- a/tests/test_reports_ledger.py +++ b/tests/test_reports_ledger.py @@ -109,10 +109,12 @@ class ExpectedPostings(core.RelatedPostings): raise NoHeader(account) else: return + closing_bal = norm_func(expect_posts.balance_at_cost()) if account.is_under('Assets', 'Equity', 'Liabilities'): opening_row = testutil.ODSCell.from_row(next(rows)) assert opening_row[0].value == start_date assert opening_row[4].text == open_bal.format(None, empty='0', sep='\0') + closing_bal += open_bal for expected in expect_posts: cells = iter(testutil.ODSCell.from_row(next(rows))) assert next(cells).value == expected.meta.date @@ -125,7 +127,6 @@ class ExpectedPostings(core.RelatedPostings): assert next(cells).value == norm_func(expected.units.number) assert next(cells).value == norm_func(expected.at_cost().number) closing_row = testutil.ODSCell.from_row(next(rows)) - closing_bal = open_bal + norm_func(expect_posts.balance_at_cost()) assert closing_row[0].value == end_date assert closing_row[4].text == closing_bal.format(None, empty='0', sep='\0')