From 5e147dc0b5577dc5943ab30f8dc5a0b8cec10424 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Fri, 16 Oct 2020 11:56:38 -0400 Subject: [PATCH] core: Balances lets the caller specify posting metadata to load. --- .../reports/balance_sheet.py | 11 ++-- conservancy_beancount/reports/core.py | 18 +++--- tests/test_reports_balances.py | 58 ++++++++++++++----- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/conservancy_beancount/reports/balance_sheet.py b/conservancy_beancount/reports/balance_sheet.py index cac8c22..d5f3549 100644 --- a/conservancy_beancount/reports/balance_sheet.py +++ b/conservancy_beancount/reports/balance_sheet.py @@ -299,10 +299,10 @@ class Report(core.BaseODS[Sequence[None], None]): ("Fundraising", 'fundraising'), ]: period_bal = self.balances.total( - account='Expenses', period=Period.PERIOD, post_type=type_value, + account='Expenses', period=Period.PERIOD, post_meta=type_value, ) prior_bal = self.balances.total( - account='Expenses', period=Period.PRIOR, post_type=type_value, + account='Expenses', period=Period.PRIOR, post_meta=type_value, ) self.write_totals_row(text, [ period_bal, @@ -359,9 +359,9 @@ class Report(core.BaseODS[Sequence[None], None]): totals_prefix=[f"Total {self.period_desc} Ended"], ) totals = self.write_classifications_by_account('Expenses', [ - {'period': Period.PERIOD, 'post_type': 'program'}, - {'period': Period.PERIOD, 'post_type': 'management'}, - {'period': Period.PERIOD, 'post_type': 'fundraising'}, + {'period': Period.PERIOD, 'post_meta': 'program'}, + {'period': Period.PERIOD, 'post_meta': 'management'}, + {'period': Period.PERIOD, 'post_meta': 'fundraising'}, {'period': Period.PERIOD}, {'period': Period.PRIOR}, ]) @@ -562,6 +562,7 @@ def main(arglist: Optional[Sequence[str]]=None, postings, args.start_date, args.stop_date, + 'expense-type', args.fund_metadata_key, args.unrestricted_fund, ) diff --git a/conservancy_beancount/reports/core.py b/conservancy_beancount/reports/core.py index 9512bfa..bb05b83 100644 --- a/conservancy_beancount/reports/core.py +++ b/conservancy_beancount/reports/core.py @@ -298,7 +298,7 @@ class BalanceKey(NamedTuple): classification: data.Account period: Period fund: Fund - post_type: Optional[str] + post_meta: Optional[str] class Balances: @@ -316,7 +316,8 @@ class Balances: postings: Iterable[data.Posting], start_date: datetime.date, stop_date: datetime.date, - fund_key: str='project', + post_meta_key: MetaKey, + fund_key: MetaKey='project', unrestricted_fund_value: str='Conservancy', ) -> None: year_diff = (stop_date - start_date).days // 365 @@ -360,11 +361,8 @@ class Balances: raise TypeError() except (KeyError, TypeError): classification = post.account - if post.account.root_part() == 'Expenses': - post_type = post.meta.get('expense-type') - else: - post_type = None - key = BalanceKey(post.account, classification, period, fund, post_type) + post_meta = post.meta.get(post_meta_key) + key = BalanceKey(post.account, classification, period, fund, post_meta) self.balances[key] += post.at_cost() def total(self, @@ -372,7 +370,7 @@ class Balances: classification: Optional[str]=None, period: int=Period.ANY, fund: int=Fund.ANY, - post_type: Optional[str]=None, + post_meta: Optional[str]=None, *, account_exact: bool=False, ) -> Balance: @@ -383,7 +381,7 @@ class Balances: pass ``account_exact=True``, the postings must have exactly the ``account`` you specify instead. - Given ``period``, ``fund``, or ``post_type`` criteria, limits to + Given ``period``, ``fund``, or ``post_meta`` criteria, limits to reporting the balance of postings that match that reporting period, fund type, or metadata value, respectively. """ @@ -411,7 +409,7 @@ class Balances: pass elif not fund & key.fund: pass - elif not (post_type is None or post_type == key.post_type): + elif not (post_meta is None or post_meta == key.post_meta): pass else: retval += balance diff --git a/tests/test_reports_balances.py b/tests/test_reports_balances.py index 925e943..a6b1a05 100644 --- a/tests/test_reports_balances.py +++ b/tests/test_reports_balances.py @@ -32,11 +32,11 @@ Period = core.Period clean_account_meta = pytest.fixture(scope='module')(testutil.clean_account_meta) @pytest.fixture(scope='module') -def income_expense_balances(): +def income_expense_entries(): txns = [] prior_date = datetime.date(2019, 2, 2) period_date = datetime.date(2019, 4, 4) - for (acct, post_type), fund in itertools.product([ + for (acct, post_meta), fund in itertools.product([ ('Income:Donations', 'Donations'), ('Income:Sales', 'RBI'), ('Expenses:Postage', 'fundraising'), @@ -56,7 +56,7 @@ def income_expense_balances(): )) meta = { 'project': fund, - f'{root_acct.lower().rstrip("s")}-type': post_type, + f'{root_acct.lower().rstrip("s")}-type': post_meta, } sign = '' if root_acct == 'Expenses' else '-' txns.append(testutil.Transaction(date=prior_date, postings=[ @@ -65,10 +65,24 @@ def income_expense_balances(): txns.append(testutil.Transaction(date=period_date, postings=[ (acct, f'{sign}2.60', meta), ])) + return txns + +@pytest.fixture(scope='module') +def expense_balances(income_expense_entries): return core.Balances( - data.Posting.from_entries(txns), + data.Posting.from_entries(income_expense_entries), datetime.date(2019, 3, 1), datetime.date(2020, 3, 1), + 'expense-type', + ) + +@pytest.fixture(scope='module') +def income_balances(income_expense_entries): + return core.Balances( + data.Posting.from_entries(income_expense_entries), + datetime.date(2019, 3, 1), + datetime.date(2020, 3, 1), + 'income-type', ) @pytest.mark.parametrize('kwargs,expected', [ @@ -82,18 +96,36 @@ def income_expense_balances(): ({'period': Period.PERIOD, 'account': 'Expenses'}, 26), ({'fund': Fund.RESTRICTED, 'account': 'Income'}, -10), ({'fund': Fund.UNRESTRICTED, 'account': 'Expenses'}, 25), - ({'post_type': 'fundraising'}, 20), - ({'post_type': 'management'}, 10), - ({'post_type': 'Nonexistent'}, None), - ({'period': Period.PRIOR, 'post_type': 'fundraising'}, '9.60'), - ({'fund': Fund.RESTRICTED, 'post_type': 'program'}, 10), - ({'period': Period.PRIOR, 'fund': Fund.RESTRICTED, 'post_type': 'program'}, '4.80'), - ({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_type': 'ø'}, None), + ({'post_meta': 'fundraising'}, 20), + ({'post_meta': 'management'}, 10), + ({'post_meta': 'Donations'}, None), + ({'post_meta': 'RBI'}, None), + ({'period': Period.PRIOR, 'post_meta': 'fundraising'}, '9.60'), + ({'fund': Fund.RESTRICTED, 'post_meta': 'program'}, 10), + ({'period': Period.PRIOR, 'fund': Fund.RESTRICTED, 'post_meta': 'program'}, '4.80'), + ({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_meta': 'ø'}, None), ({'account': ('Income', 'Expenses')}, 30), ({'account': ('Income', 'Expenses'), 'fund': Fund.UNRESTRICTED}, 15), ]) -def test_balance_total(income_expense_balances, kwargs, expected): - actual = income_expense_balances.total(**kwargs) +def test_expense_balance_total(expense_balances, kwargs, expected): + actual = expense_balances.total(**kwargs) + if expected is None: + assert not actual + else: + assert actual == {'USD': testutil.Amount(expected)} + +@pytest.mark.parametrize('kwargs,expected', [ + ({'post_meta': 'fundraising'}, None), + ({'post_meta': 'management'}, None), + ({'post_meta': 'Donations'}, -10), + ({'post_meta': 'RBI'}, -10), + ({'period': Period.PRIOR, 'post_meta': 'Donations'}, '-4.80'), + ({'fund': Fund.RESTRICTED, 'post_meta': 'RBI'}, -5), + ({'period': Period.PRIOR, 'fund': Fund.RESTRICTED, 'post_meta': 'Donations'}, '-2.40'), + ({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_meta': 'ø'}, None), +]) +def test_income_balance_total(income_balances, kwargs, expected): + actual = income_balances.total(**kwargs) if expected is None: assert not actual else: