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]
|
AccountsMap = Mapping[data.Account, core.PeriodPostings]
|
||||||
FundPosts = Tuple[MetaValue, AccountsMap]
|
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'
|
PROGNAME = 'fund-report'
|
||||||
UNRESTRICTED_FUND = 'Conservancy'
|
UNRESTRICTED_FUND = 'Conservancy'
|
||||||
logger = logging.getLogger('conservancy_beancount.reports.fund')
|
logger = logging.getLogger('conservancy_beancount.reports.fund')
|
||||||
|
@ -170,12 +176,17 @@ class TextReport:
|
||||||
account_map: AccountsMap,
|
account_map: AccountsMap,
|
||||||
) -> Iterator[Tuple[str, Sequence[str]]]:
|
) -> Iterator[Tuple[str, Sequence[str]]]:
|
||||||
total_fmt = f'{fund} balance as of {{}}'
|
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:
|
if acct_s is core.OPENING_BALANCE_NAME:
|
||||||
acct_s = total_fmt.format(self.start_date.isoformat())
|
acct_s = total_fmt.format(self.start_date.isoformat())
|
||||||
elif acct_s is core.ENDING_BALANCE_NAME:
|
elif acct_s is core.ENDING_BALANCE_NAME:
|
||||||
acct_s = total_fmt.format(self.stop_date.isoformat())
|
acct_s = total_fmt.format(self.stop_date.isoformat())
|
||||||
yield acct_s, (-balance).format(None, sep='\0').split('\0')
|
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:
|
def write(self, rows: Iterable[FundPosts]) -> None:
|
||||||
output = [
|
output = [
|
||||||
|
@ -319,7 +330,6 @@ def main(arglist: Optional[Sequence[str]]=None,
|
||||||
post
|
post
|
||||||
for post in data.Posting.from_entries(entries)
|
for post in data.Posting.from_entries(entries)
|
||||||
if post.meta.date < args.stop_date
|
if post.meta.date < args.stop_date
|
||||||
and post.account.is_under(*BALANCE_ACCOUNTS)
|
|
||||||
)
|
)
|
||||||
for search_term in args.search_terms:
|
for search_term in args.search_terms:
|
||||||
postings = search_term.filter_postings(postings)
|
postings = search_term.filter_postings(postings)
|
||||||
|
|
|
@ -40,6 +40,8 @@ START_DATE = datetime.date(2018, 3, 1)
|
||||||
MID_DATE = datetime.date(2019, 3, 1)
|
MID_DATE = datetime.date(2019, 3, 1)
|
||||||
STOP_DATE = datetime.date(2020, 3, 1)
|
STOP_DATE = datetime.date(2020, 3, 1)
|
||||||
|
|
||||||
|
EQUITY_ROOT_ACCOUNTS = ('Expenses:', 'Equity:', 'Income:')
|
||||||
|
|
||||||
OPENING_BALANCES = {
|
OPENING_BALANCES = {
|
||||||
'Alpha': 3000,
|
'Alpha': 3000,
|
||||||
'Bravo': 2000,
|
'Bravo': 2000,
|
||||||
|
@ -51,18 +53,26 @@ BALANCES_BY_YEAR = {
|
||||||
('Conservancy', 2018): [
|
('Conservancy', 2018): [
|
||||||
('Income:Other', 40),
|
('Income:Other', 40),
|
||||||
('Expenses:Other', -4),
|
('Expenses:Other', -4),
|
||||||
|
('Assets:Receivable:Accounts', 40),
|
||||||
|
('Liabilities:Payable:Accounts', 4),
|
||||||
],
|
],
|
||||||
('Conservancy', 2019): [
|
('Conservancy', 2019): [
|
||||||
('Income:Other', 42),
|
('Income:Other', 42),
|
||||||
('Expenses:Other', Decimal('-4.20')),
|
('Expenses:Other', Decimal('-4.20')),
|
||||||
('Equity:Realized:CurrencyConversion', Decimal('6.20')),
|
('Equity:Realized:CurrencyConversion', Decimal('6.20')),
|
||||||
|
('Assets:Receivable:Accounts', -40),
|
||||||
|
('Liabilities:Payable:Accounts', -4),
|
||||||
],
|
],
|
||||||
('Alpha', 2018): [
|
('Alpha', 2018): [
|
||||||
('Income:Other', 60),
|
('Income:Other', 60),
|
||||||
|
('Liabilities:UnearnedIncome', 30),
|
||||||
|
('Assets:Prepaid:Expenses', 20),
|
||||||
],
|
],
|
||||||
('Alpha', 2019): [
|
('Alpha', 2019): [
|
||||||
('Income:Other', 30),
|
('Income:Other', 30),
|
||||||
('Expenses:Other', -26),
|
('Expenses:Other', -26),
|
||||||
|
('Assets:Prepaid:Expenses', -20),
|
||||||
|
('Liabilities:UnearnedIncome', -30),
|
||||||
],
|
],
|
||||||
('Bravo', 2018): [
|
('Bravo', 2018): [
|
||||||
('Expenses:Other', -20),
|
('Expenses:Other', -20),
|
||||||
|
@ -76,14 +86,6 @@ BALANCES_BY_YEAR = {
|
||||||
def fund_entries():
|
def fund_entries():
|
||||||
return copy.deepcopy(_ledger_load[0])
|
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):
|
def split_text_lines(output):
|
||||||
for line in output:
|
for line in output:
|
||||||
account, amount = line.rsplit(None, 1)
|
account, amount = line.rsplit(None, 1)
|
||||||
|
@ -94,6 +96,17 @@ def format_amount(amount, currency='USD'):
|
||||||
amount, currency, format_type='accounting',
|
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):
|
def check_text_report(output, project, start_date, stop_date):
|
||||||
_, _, project = project.rpartition('=')
|
_, _, project = project.rpartition('=')
|
||||||
balance_amount = Decimal(OPENING_BALANCES[project])
|
balance_amount = Decimal(OPENING_BALANCES[project])
|
||||||
|
@ -105,11 +118,10 @@ def check_text_report(output, project, start_date, stop_date):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
for account, amount in amounts:
|
for account, amount in amounts:
|
||||||
if year < start_date.year:
|
if year < start_date.year and account.startswith(EQUITY_ROOT_ACCOUNTS):
|
||||||
balance_amount += amount
|
balance_amount += amount
|
||||||
else:
|
else:
|
||||||
expected[account] += amount
|
expected[account] += amount
|
||||||
expected.default_factory = None
|
|
||||||
actual = split_text_lines(output)
|
actual = split_text_lines(output)
|
||||||
next(actual); next(actual) # Discard headers
|
next(actual); next(actual) # Discard headers
|
||||||
open_acct, open_amt = next(actual)
|
open_acct, open_amt = next(actual)
|
||||||
|
@ -117,25 +129,24 @@ def check_text_report(output, project, start_date, stop_date):
|
||||||
project, start_date.isoformat(),
|
project, start_date.isoformat(),
|
||||||
)
|
)
|
||||||
assert open_amt == format_amount(balance_amount)
|
assert open_amt == format_amount(balance_amount)
|
||||||
for expect_account in [
|
balance_amount += check_text_balances(
|
||||||
|
actual, expected,
|
||||||
'Equity:Realized:CurrencyConversion',
|
'Equity:Realized:CurrencyConversion',
|
||||||
'Income:Other',
|
'Income:Other',
|
||||||
'Expenses: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)
|
end_acct, end_amt = next(actual)
|
||||||
assert end_acct == "{} balance as of {}".format(
|
assert end_acct == "{} balance as of {}".format(
|
||||||
project, stop_date.isoformat(),
|
project, stop_date.isoformat(),
|
||||||
)
|
)
|
||||||
assert end_amt == format_amount(balance_amount)
|
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
|
assert next(actual, None) is None
|
||||||
|
|
||||||
def check_ods_report(ods, start_date, stop_date):
|
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),
|
'opening': Decimal(amount),
|
||||||
'Income': Decimal(0),
|
'Income': Decimal(0),
|
||||||
'Expenses': 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 key, amount in sorted(OPENING_BALANCES.items()))
|
||||||
for fund, year in itertools.product(account_bals, range(2018, stop_date.year)):
|
for fund, year in itertools.product(account_bals, range(2018, stop_date.year)):
|
||||||
try:
|
try:
|
||||||
|
@ -152,10 +167,10 @@ def check_ods_report(ods, start_date, stop_date):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
for account, amount in amounts:
|
for account, amount in amounts:
|
||||||
if year < start_date.year:
|
if year < start_date.year and account.startswith(EQUITY_ROOT_ACCOUNTS):
|
||||||
acct_key = 'opening'
|
acct_key = 'opening'
|
||||||
else:
|
else:
|
||||||
acct_key, _, _ = account.partition(':')
|
acct_key, _, _ = account.rpartition(':')
|
||||||
account_bals[fund][acct_key] += amount
|
account_bals[fund][acct_key] += amount
|
||||||
account_bals['Unrestricted'] = account_bals.pop('Conservancy')
|
account_bals['Unrestricted'] = account_bals.pop('Conservancy')
|
||||||
for row in ods.getElementsByType(odf.table.TableRow):
|
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['opening']
|
||||||
assert next(cells).value == balances['Income']
|
assert next(cells).value == balances['Income']
|
||||||
assert next(cells).value == -balances['Expenses']
|
assert next(cells).value == -balances['Expenses']
|
||||||
if balances['Equity']:
|
if balances['Equity:Realized']:
|
||||||
assert next(cells).value == balances['Equity']
|
assert next(cells).value == balances['Equity:Realized']
|
||||||
else:
|
else:
|
||||||
assert not next(cells).value
|
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"
|
assert not account_bals, "did not see all funds in report"
|
||||||
|
|
||||||
def run_main(out_type, arglist, config=None):
|
def run_main(out_type, arglist, config=None):
|
||||||
|
|
Loading…
Reference in a new issue