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')