Changeset - 7441f4ef0ce1
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2020-06-17 22:25:47
brettcsmith@brettcsmith.org
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.
3 files changed with 39 insertions and 17 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/ledger.py
Show inline comments
...
 
@@ -66,12 +66,13 @@ from typing import (
 
)
 

	
 
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
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
...
 
@@ -81,12 +82,26 @@ from .. import rtutil
 

	
 
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',
 
        data.Metadata.human_name('entity'),
 
        'Description',
 
        'Original Amount',
...
 
@@ -265,31 +280,35 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
            for col_name in self.sheet_columns
 
        ))
 
        self.lock_first_row()
 

	
 
    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,
 
            )),
 
            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:
 
        self.add_row()
 
        self.add_row(
 
            odf.table.TableCell(),
...
 
@@ -324,17 +343,19 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
              if key in data.LINK_METADATA
 
              else self.string_cell(row.meta.get(key, ''))
 
              for key in self.metadata_columns),
 
        )
 

	
 
    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())
 
        self.add_row(
 
            self.string_cell(
 
                f"Balance as of {date.isoformat()}",
...
 
@@ -362,29 +383,29 @@ class LedgerODS(core.BaseODS[data.Posting, data.Account]):
 
            f"Ledger From {self.date_range.start.isoformat()}"
 
            f" To {self.date_range.stop.isoformat()}",
 
            stylename=self.merge_styles(self.style_centertext, self.style_bold),
 
            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))
 
            for account, related in self.account_groups.items()
 
        )
 
        tally_by_account = {
setup.py
Show inline comments
...
 
@@ -2,13 +2,13 @@
 

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

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
tests/test_reports_ledger.py
Show inline comments
...
 
@@ -106,29 +106,30 @@ class ExpectedPostings(core.RelatedPostings):
 
                break
 
        else:
 
            if expect_posts:
 
                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
 
            assert next(cells).text == (expected.meta.get('entity') or '')
 
            assert next(cells).text == (expected.meta.txn.narration or '')
 
            if expected.cost is None:
 
                assert not next(cells).text
 
                assert next(cells).value == norm_func(expected.units.number)
 
            else:
 
                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')
 

	
 

	
 
def get_sheet_names(ods):
 
    return [sheet.getAttribute('name').replace(' ', ':')
0 comments (0 inline, 0 general)