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:
parent
5e295f1024
commit
7441f4ef0c
3 changed files with 39 additions and 17 deletions
|
@ -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))
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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+',
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue