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.
This commit is contained in:
Brett Smith 2020-06-17 18:25:47 -04:00
parent 5e295f1024
commit 7441f4ef0c
3 changed files with 39 additions and 17 deletions

View file

@ -69,6 +69,7 @@ from pathlib import Path
import odf.table # type:ignore[import] import odf.table # type:ignore[import]
from beancount.core import data as bc_data
from beancount.parser import printer as bc_printer from beancount.parser import printer as bc_printer
from . import core from . import core
@ -84,6 +85,20 @@ PostTally = List[Tuple[int, data.Account]]
PROGNAME = 'ledger-report' PROGNAME = 'ledger-report'
logger = logging.getLogger('conservancy_beancount.reports.ledger') 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]): class LedgerODS(core.BaseODS[data.Posting, data.Account]):
CORE_COLUMNS: Sequence[str] = [ CORE_COLUMNS: Sequence[str] = [
'Date', '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: def _report_section_balance(self, key: data.Account, date_key: str) -> None:
uses_opening = key.is_under('Assets', 'Equity', 'Liabilities') uses_opening = key.is_under('Assets', 'Equity', 'Liabilities')
related = self.account_groups[key]
if date_key == 'start': if date_key == 'start':
if not uses_opening: if not uses_opening:
return return
date = self.date_range.start date = self.date_range.start
balance = related.start_bal
description = "Opening Balance" description = "Opening Balance"
else: else:
date = self.date_range.stop date = self.date_range.stop
description = "Ending Balance" if uses_opening else "Period Total" if uses_opening:
balance = self.norm_func( balance = related.stop_bal
self.account_groups[key].balance_at_cost_by_date(date) description = "Ending Balance"
) else:
balance = related.period_bal
description = "Period Total"
self.add_row( self.add_row(
self.date_cell(date, stylename=self.merge_styles( self.date_cell(date, stylename=self.merge_styles(
self.style_bold, self.style_date, self.style_bold, self.style_date,
@ -286,7 +305,7 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
odf.table.TableCell(), odf.table.TableCell(),
self.string_cell(description, stylename=self.style_bold), self.string_cell(description, stylename=self.style_bold),
odf.table.TableCell(), 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: 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, def _combined_balance_row(self,
date: datetime.date,
balance_accounts: Sequence[str], balance_accounts: Sequence[str],
attr_name: str,
) -> None: ) -> None:
date = getattr(self.date_range, attr_name)
balance_attrname = f'{attr_name}_bal'
balance = -sum(( balance = -sum((
related.balance_at_cost_by_date(date) getattr(related, balance_attrname)
for account, related in self.account_groups.items() for account, related in self.account_groups.items()
if account.is_under(*balance_accounts) if account.is_under(*balance_accounts)
), core.MutableBalance()) ), core.MutableBalance())
@ -365,23 +386,23 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
numbercolumnsspanned=2, numbercolumnsspanned=2,
)) ))
self.add_row() 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( for _, account in self._sort_and_filter_accounts(
self.account_groups, balance_accounts, self.account_groups, balance_accounts,
): ):
related = self.account_groups[account] balance = self.account_groups[account].period_bal
# 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)
if not balance.is_zero(): if not balance.is_zero():
self.add_row( self.add_row(
self.string_cell(account, stylename=self.style_endtext), 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: 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() self.write_balance_sheet()
tally_by_account_iter = ( tally_by_account_iter = (
(account, sum(1 for post in related if post.meta.date in self.date_range)) (account, sum(1 for post in related if post.meta.date in self.date_range))

View file

@ -5,7 +5,7 @@ from setuptools import setup
setup( setup(
name='conservancy_beancount', name='conservancy_beancount',
description="Plugin, library, and reports for reading Conservancy's books", description="Plugin, library, and reports for reading Conservancy's books",
version='1.2.5', version='1.2.6',
author='Software Freedom Conservancy', author='Software Freedom Conservancy',
author_email='info@sfconservancy.org', author_email='info@sfconservancy.org',
license='GNU AGPLv3+', license='GNU AGPLv3+',

View file

@ -109,10 +109,12 @@ class ExpectedPostings(core.RelatedPostings):
raise NoHeader(account) raise NoHeader(account)
else: else:
return return
closing_bal = norm_func(expect_posts.balance_at_cost())
if account.is_under('Assets', 'Equity', 'Liabilities'): if account.is_under('Assets', 'Equity', 'Liabilities'):
opening_row = testutil.ODSCell.from_row(next(rows)) opening_row = testutil.ODSCell.from_row(next(rows))
assert opening_row[0].value == start_date assert opening_row[0].value == start_date
assert opening_row[4].text == open_bal.format(None, empty='0', sep='\0') assert opening_row[4].text == open_bal.format(None, empty='0', sep='\0')
closing_bal += open_bal
for expected in expect_posts: for expected in expect_posts:
cells = iter(testutil.ODSCell.from_row(next(rows))) cells = iter(testutil.ODSCell.from_row(next(rows)))
assert next(cells).value == expected.meta.date 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.units.number)
assert next(cells).value == norm_func(expected.at_cost().number) assert next(cells).value == norm_func(expected.at_cost().number)
closing_row = testutil.ODSCell.from_row(next(rows)) 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[0].value == end_date
assert closing_row[4].text == closing_bal.format(None, empty='0', sep='\0') assert closing_row[4].text == closing_bal.format(None, empty='0', sep='\0')