core: Balances lets the caller specify posting metadata to load.
This commit is contained in:
parent
ffc20b6899
commit
5e147dc0b5
3 changed files with 59 additions and 28 deletions
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue