core: Balances lets the caller specify posting metadata to load.

This commit is contained in:
Brett Smith 2020-10-16 11:56:38 -04:00
parent ffc20b6899
commit 5e147dc0b5
3 changed files with 59 additions and 28 deletions

View file

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

View file

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

View file

@ -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: