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

View file

@ -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+',

View file

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