From 7441f4ef0ce1448f751f0754c22dac53d4a5d2b9 2020-06-17 22:25:47 From: Brett Smith Date: 2020-06-17 22:25:47 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. --- diff --git a/conservancy_beancount/reports/ledger.py b/conservancy_beancount/reports/ledger.py index b69c18020ac4118fa8fc85291304d4ea9c3eef1e..e28c0984e636ce96daba9ffdd44af7ef2df13450 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 385fa5bbbef687b5e9ed0d78b408fc27d445ecbf..911a3da1149c1ec83601b3d1196087fb5506435f 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 00df81a388d8dbfd9d11c8e9bd8de901a7152fbd..db65abd5757dd9230e761eb26b43a44064744178 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')