fund: Add outstanding balances to text fund report.

This commit is contained in:
Brett Smith 2020-06-27 16:10:27 -04:00
parent b1a46d6ef6
commit 9ae974009b
2 changed files with 60 additions and 33 deletions

View file

@ -80,7 +80,13 @@ from .. import data
AccountsMap = Mapping[data.Account, core.PeriodPostings]
FundPosts = Tuple[MetaValue, AccountsMap]
BALANCE_ACCOUNTS = ['Equity', 'Income', 'Expenses']
EQUITY_ACCOUNTS = ['Equity', 'Income', 'Expenses']
INFO_ACCOUNTS = [
'Assets:Receivable',
'Assets:Prepaid',
'Liabilities:Payable',
'Liabilities:UnearnedIncome',
]
PROGNAME = 'fund-report'
UNRESTRICTED_FUND = 'Conservancy'
logger = logging.getLogger('conservancy_beancount.reports.fund')
@ -170,12 +176,17 @@ class TextReport:
account_map: AccountsMap,
) -> Iterator[Tuple[str, Sequence[str]]]:
total_fmt = f'{fund} balance as of {{}}'
for acct_s, balance in core.account_balances(account_map, BALANCE_ACCOUNTS):
for acct_s, balance in core.account_balances(account_map, EQUITY_ACCOUNTS):
if acct_s is core.OPENING_BALANCE_NAME:
acct_s = total_fmt.format(self.start_date.isoformat())
elif acct_s is core.ENDING_BALANCE_NAME:
acct_s = total_fmt.format(self.stop_date.isoformat())
yield acct_s, (-balance).format(None, sep='\0').split('\0')
for _, account in core.sort_and_filter_accounts(account_map, INFO_ACCOUNTS):
balance = account_map[account].stop_bal
if not balance.is_zero():
balance = core.normalize_amount_func(account)(balance)
yield account, balance.format(None, sep='\0').split('\0')
def write(self, rows: Iterable[FundPosts]) -> None:
output = [
@ -319,7 +330,6 @@ def main(arglist: Optional[Sequence[str]]=None,
post
for post in data.Posting.from_entries(entries)
if post.meta.date < args.stop_date
and post.account.is_under(*BALANCE_ACCOUNTS)
)
for search_term in args.search_terms:
postings = search_term.filter_postings(postings)

View file

@ -40,6 +40,8 @@ START_DATE = datetime.date(2018, 3, 1)
MID_DATE = datetime.date(2019, 3, 1)
STOP_DATE = datetime.date(2020, 3, 1)
EQUITY_ROOT_ACCOUNTS = ('Expenses:', 'Equity:', 'Income:')
OPENING_BALANCES = {
'Alpha': 3000,
'Bravo': 2000,
@ -51,18 +53,26 @@ BALANCES_BY_YEAR = {
('Conservancy', 2018): [
('Income:Other', 40),
('Expenses:Other', -4),
('Assets:Receivable:Accounts', 40),
('Liabilities:Payable:Accounts', 4),
],
('Conservancy', 2019): [
('Income:Other', 42),
('Expenses:Other', Decimal('-4.20')),
('Equity:Realized:CurrencyConversion', Decimal('6.20')),
('Assets:Receivable:Accounts', -40),
('Liabilities:Payable:Accounts', -4),
],
('Alpha', 2018): [
('Income:Other', 60),
('Liabilities:UnearnedIncome', 30),
('Assets:Prepaid:Expenses', 20),
],
('Alpha', 2019): [
('Income:Other', 30),
('Expenses:Other', -26),
('Assets:Prepaid:Expenses', -20),
('Liabilities:UnearnedIncome', -30),
],
('Bravo', 2018): [
('Expenses:Other', -20),
@ -76,14 +86,6 @@ BALANCES_BY_YEAR = {
def fund_entries():
return copy.deepcopy(_ledger_load[0])
def fund_postings(entries, project, stop_date):
return (
post for post in data.Posting.from_entries(entries)
if post.meta.date < stop_date
and post.account.is_under('Equity', 'Income', 'Expenses')
and post.meta.get('project') == project
)
def split_text_lines(output):
for line in output:
account, amount = line.rsplit(None, 1)
@ -94,6 +96,17 @@ def format_amount(amount, currency='USD'):
amount, currency, format_type='accounting',
)
def check_text_balances(actual, expected, *expect_accounts):
balance = Decimal()
for expect_account in expect_accounts:
expect_amount = expected[expect_account]
if expect_amount:
actual_account, actual_amount = next(actual)
assert actual_account == expect_account
assert actual_amount == format_amount(expect_amount)
balance += expect_amount
return balance
def check_text_report(output, project, start_date, stop_date):
_, _, project = project.rpartition('=')
balance_amount = Decimal(OPENING_BALANCES[project])
@ -105,11 +118,10 @@ def check_text_report(output, project, start_date, stop_date):
pass
else:
for account, amount in amounts:
if year < start_date.year:
if year < start_date.year and account.startswith(EQUITY_ROOT_ACCOUNTS):
balance_amount += amount
else:
expected[account] += amount
expected.default_factory = None
actual = split_text_lines(output)
next(actual); next(actual) # Discard headers
open_acct, open_amt = next(actual)
@ -117,25 +129,24 @@ def check_text_report(output, project, start_date, stop_date):
project, start_date.isoformat(),
)
assert open_amt == format_amount(balance_amount)
for expect_account in [
balance_amount += check_text_balances(
actual, expected,
'Equity:Realized:CurrencyConversion',
'Income:Other',
'Expenses:Other',
]:
try:
expect_amount = expected[expect_account]
except KeyError:
continue
else:
actual_account, actual_amount = next(actual)
assert actual_account == expect_account
assert actual_amount == format_amount(expect_amount)
balance_amount += expect_amount
)
end_acct, end_amt = next(actual)
assert end_acct == "{} balance as of {}".format(
project, stop_date.isoformat(),
)
assert end_amt == format_amount(balance_amount)
balance_amount += check_text_balances(
actual, expected,
'Assets:Receivable:Accounts',
'Assets:Prepaid:Expenses',
'Liabilities:Payable:Accounts',
'Liabilities:UnearnedIncome',
)
assert next(actual, None) is None
def check_ods_report(ods, start_date, stop_date):
@ -143,7 +154,11 @@ def check_ods_report(ods, start_date, stop_date):
'opening': Decimal(amount),
'Income': Decimal(0),
'Expenses': Decimal(0),
'Equity': Decimal(0),
'Equity:Realized': Decimal(0),
'Assets:Receivable': Decimal(0),
'Assets:Prepaid': Decimal(0),
'Liabilities:Payable': Decimal(0),
'Liabilities': Decimal(0), # UnearnedIncome
}) for key, amount in sorted(OPENING_BALANCES.items()))
for fund, year in itertools.product(account_bals, range(2018, stop_date.year)):
try:
@ -152,10 +167,10 @@ def check_ods_report(ods, start_date, stop_date):
pass
else:
for account, amount in amounts:
if year < start_date.year:
if year < start_date.year and account.startswith(EQUITY_ROOT_ACCOUNTS):
acct_key = 'opening'
else:
acct_key, _, _ = account.partition(':')
acct_key, _, _ = account.rpartition(':')
account_bals[fund][acct_key] += amount
account_bals['Unrestricted'] = account_bals.pop('Conservancy')
for row in ods.getElementsByType(odf.table.TableRow):
@ -169,11 +184,13 @@ def check_ods_report(ods, start_date, stop_date):
assert next(cells).value == balances['opening']
assert next(cells).value == balances['Income']
assert next(cells).value == -balances['Expenses']
if balances['Equity']:
assert next(cells).value == balances['Equity']
if balances['Equity:Realized']:
assert next(cells).value == balances['Equity:Realized']
else:
assert not next(cells).value
assert next(cells).value == sum(balances.values())
assert next(cells).value == sum(balances[key] for key in [
'opening', 'Income', 'Expenses', 'Equity:Realized',
])
assert not account_bals, "did not see all funds in report"
def run_main(out_type, arglist, config=None):