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'),
|
("Fundraising", 'fundraising'),
|
||||||
]:
|
]:
|
||||||
period_bal = self.balances.total(
|
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(
|
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, [
|
self.write_totals_row(text, [
|
||||||
period_bal,
|
period_bal,
|
||||||
|
@ -359,9 +359,9 @@ class Report(core.BaseODS[Sequence[None], None]):
|
||||||
totals_prefix=[f"Total {self.period_desc} Ended"],
|
totals_prefix=[f"Total {self.period_desc} Ended"],
|
||||||
)
|
)
|
||||||
totals = self.write_classifications_by_account('Expenses', [
|
totals = self.write_classifications_by_account('Expenses', [
|
||||||
{'period': Period.PERIOD, 'post_type': 'program'},
|
{'period': Period.PERIOD, 'post_meta': 'program'},
|
||||||
{'period': Period.PERIOD, 'post_type': 'management'},
|
{'period': Period.PERIOD, 'post_meta': 'management'},
|
||||||
{'period': Period.PERIOD, 'post_type': 'fundraising'},
|
{'period': Period.PERIOD, 'post_meta': 'fundraising'},
|
||||||
{'period': Period.PERIOD},
|
{'period': Period.PERIOD},
|
||||||
{'period': Period.PRIOR},
|
{'period': Period.PRIOR},
|
||||||
])
|
])
|
||||||
|
@ -562,6 +562,7 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
postings,
|
postings,
|
||||||
args.start_date,
|
args.start_date,
|
||||||
args.stop_date,
|
args.stop_date,
|
||||||
|
'expense-type',
|
||||||
args.fund_metadata_key,
|
args.fund_metadata_key,
|
||||||
args.unrestricted_fund,
|
args.unrestricted_fund,
|
||||||
)
|
)
|
||||||
|
|
|
@ -298,7 +298,7 @@ class BalanceKey(NamedTuple):
|
||||||
classification: data.Account
|
classification: data.Account
|
||||||
period: Period
|
period: Period
|
||||||
fund: Fund
|
fund: Fund
|
||||||
post_type: Optional[str]
|
post_meta: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Balances:
|
class Balances:
|
||||||
|
@ -316,7 +316,8 @@ class Balances:
|
||||||
postings: Iterable[data.Posting],
|
postings: Iterable[data.Posting],
|
||||||
start_date: datetime.date,
|
start_date: datetime.date,
|
||||||
stop_date: datetime.date,
|
stop_date: datetime.date,
|
||||||
fund_key: str='project',
|
post_meta_key: MetaKey,
|
||||||
|
fund_key: MetaKey='project',
|
||||||
unrestricted_fund_value: str='Conservancy',
|
unrestricted_fund_value: str='Conservancy',
|
||||||
) -> None:
|
) -> None:
|
||||||
year_diff = (stop_date - start_date).days // 365
|
year_diff = (stop_date - start_date).days // 365
|
||||||
|
@ -360,11 +361,8 @@ class Balances:
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
classification = post.account
|
classification = post.account
|
||||||
if post.account.root_part() == 'Expenses':
|
post_meta = post.meta.get(post_meta_key)
|
||||||
post_type = post.meta.get('expense-type')
|
key = BalanceKey(post.account, classification, period, fund, post_meta)
|
||||||
else:
|
|
||||||
post_type = None
|
|
||||||
key = BalanceKey(post.account, classification, period, fund, post_type)
|
|
||||||
self.balances[key] += post.at_cost()
|
self.balances[key] += post.at_cost()
|
||||||
|
|
||||||
def total(self,
|
def total(self,
|
||||||
|
@ -372,7 +370,7 @@ class Balances:
|
||||||
classification: Optional[str]=None,
|
classification: Optional[str]=None,
|
||||||
period: int=Period.ANY,
|
period: int=Period.ANY,
|
||||||
fund: int=Fund.ANY,
|
fund: int=Fund.ANY,
|
||||||
post_type: Optional[str]=None,
|
post_meta: Optional[str]=None,
|
||||||
*,
|
*,
|
||||||
account_exact: bool=False,
|
account_exact: bool=False,
|
||||||
) -> Balance:
|
) -> Balance:
|
||||||
|
@ -383,7 +381,7 @@ class Balances:
|
||||||
pass ``account_exact=True``, the postings must have exactly the
|
pass ``account_exact=True``, the postings must have exactly the
|
||||||
``account`` you specify instead.
|
``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,
|
reporting the balance of postings that match that reporting period,
|
||||||
fund type, or metadata value, respectively.
|
fund type, or metadata value, respectively.
|
||||||
"""
|
"""
|
||||||
|
@ -411,7 +409,7 @@ class Balances:
|
||||||
pass
|
pass
|
||||||
elif not fund & key.fund:
|
elif not fund & key.fund:
|
||||||
pass
|
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
|
pass
|
||||||
else:
|
else:
|
||||||
retval += balance
|
retval += balance
|
||||||
|
|
|
@ -32,11 +32,11 @@ Period = core.Period
|
||||||
clean_account_meta = pytest.fixture(scope='module')(testutil.clean_account_meta)
|
clean_account_meta = pytest.fixture(scope='module')(testutil.clean_account_meta)
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def income_expense_balances():
|
def income_expense_entries():
|
||||||
txns = []
|
txns = []
|
||||||
prior_date = datetime.date(2019, 2, 2)
|
prior_date = datetime.date(2019, 2, 2)
|
||||||
period_date = datetime.date(2019, 4, 4)
|
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:Donations', 'Donations'),
|
||||||
('Income:Sales', 'RBI'),
|
('Income:Sales', 'RBI'),
|
||||||
('Expenses:Postage', 'fundraising'),
|
('Expenses:Postage', 'fundraising'),
|
||||||
|
@ -56,7 +56,7 @@ def income_expense_balances():
|
||||||
))
|
))
|
||||||
meta = {
|
meta = {
|
||||||
'project': fund,
|
'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 '-'
|
sign = '' if root_acct == 'Expenses' else '-'
|
||||||
txns.append(testutil.Transaction(date=prior_date, postings=[
|
txns.append(testutil.Transaction(date=prior_date, postings=[
|
||||||
|
@ -65,10 +65,24 @@ def income_expense_balances():
|
||||||
txns.append(testutil.Transaction(date=period_date, postings=[
|
txns.append(testutil.Transaction(date=period_date, postings=[
|
||||||
(acct, f'{sign}2.60', meta),
|
(acct, f'{sign}2.60', meta),
|
||||||
]))
|
]))
|
||||||
|
return txns
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def expense_balances(income_expense_entries):
|
||||||
return core.Balances(
|
return core.Balances(
|
||||||
data.Posting.from_entries(txns),
|
data.Posting.from_entries(income_expense_entries),
|
||||||
datetime.date(2019, 3, 1),
|
datetime.date(2019, 3, 1),
|
||||||
datetime.date(2020, 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', [
|
@pytest.mark.parametrize('kwargs,expected', [
|
||||||
|
@ -82,18 +96,36 @@ def income_expense_balances():
|
||||||
({'period': Period.PERIOD, 'account': 'Expenses'}, 26),
|
({'period': Period.PERIOD, 'account': 'Expenses'}, 26),
|
||||||
({'fund': Fund.RESTRICTED, 'account': 'Income'}, -10),
|
({'fund': Fund.RESTRICTED, 'account': 'Income'}, -10),
|
||||||
({'fund': Fund.UNRESTRICTED, 'account': 'Expenses'}, 25),
|
({'fund': Fund.UNRESTRICTED, 'account': 'Expenses'}, 25),
|
||||||
({'post_type': 'fundraising'}, 20),
|
({'post_meta': 'fundraising'}, 20),
|
||||||
({'post_type': 'management'}, 10),
|
({'post_meta': 'management'}, 10),
|
||||||
({'post_type': 'Nonexistent'}, None),
|
({'post_meta': 'Donations'}, None),
|
||||||
({'period': Period.PRIOR, 'post_type': 'fundraising'}, '9.60'),
|
({'post_meta': 'RBI'}, None),
|
||||||
({'fund': Fund.RESTRICTED, 'post_type': 'program'}, 10),
|
({'period': Period.PRIOR, 'post_meta': 'fundraising'}, '9.60'),
|
||||||
({'period': Period.PRIOR, 'fund': Fund.RESTRICTED, 'post_type': 'program'}, '4.80'),
|
({'fund': Fund.RESTRICTED, 'post_meta': 'program'}, 10),
|
||||||
({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_type': 'ø'}, None),
|
({'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')}, 30),
|
||||||
({'account': ('Income', 'Expenses'), 'fund': Fund.UNRESTRICTED}, 15),
|
({'account': ('Income', 'Expenses'), 'fund': Fund.UNRESTRICTED}, 15),
|
||||||
])
|
])
|
||||||
def test_balance_total(income_expense_balances, kwargs, expected):
|
def test_expense_balance_total(expense_balances, kwargs, expected):
|
||||||
actual = income_expense_balances.total(**kwargs)
|
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:
|
if expected is None:
|
||||||
assert not actual
|
assert not actual
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Add table
Reference in a new issue