fund: Add outstanding balances to text fund report.
This commit is contained in:
parent
b1a46d6ef6
commit
9ae974009b
2 changed files with 60 additions and 33 deletions
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue