conservancy_beancount/tests/test_reports_core.py

209 lines
7.1 KiB
Python

"""test_reports_core - Unit tests for basic reports functions"""
# Copyright © 2020 Brett Smith
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
#
# Full copyright and licensing details can be found at toplevel file
# LICENSE.txt in the repository.
import datetime
import pytest
from decimal import Decimal
from . import testutil
from conservancy_beancount import data
from conservancy_beancount.reports import core
AMOUNTS = [
2,
Decimal('4.40'),
testutil.Amount('6.60', 'CHF'),
core.Balance([testutil.Amount('8.80')]),
]
@pytest.fixture
def balance_postings():
dates = testutil.date_seq(testutil.FY_MID_DATE)
return data.Posting.from_entries([
testutil.Transaction(date=next(dates), postings=[
('Equity:OpeningBalance', -1000),
('Assets:Checking', 1000),
]),
testutil.Transaction(date=next(dates), postings=[
('Income:Donations', -10),
('Expenses:BankingFees', 1),
('Assets:Checking', 9),
]),
testutil.Transaction(date=next(dates), postings=[
('Income:Donations', -20),
('Expenses:Services:Fundraising', 1),
('Equity:Realized:CurrencyConversion', 1),
('Assets:Checking', 18),
]),
])
@pytest.mark.parametrize('acct_name', [
'Assets:Checking',
'Assets:Receivable:Accounts',
'Expenses:Other',
'Expenses:FilingFees',
])
def test_normalize_amount_func_pos(acct_name):
actual = core.normalize_amount_func(acct_name)
for amount in AMOUNTS:
assert actual(amount) == amount
@pytest.mark.parametrize('acct_name', [
'Equity:Funds:Restricted',
'Equity:Realized:CurrencyConversion',
'Income:Donations',
'Income:Other',
'Liabilities:CreditCard',
'Liabilities:Payable:Accounts',
])
def test_normalize_amount_func_neg(acct_name):
actual = core.normalize_amount_func(acct_name)
for amount in AMOUNTS:
assert actual(amount) == -amount
@pytest.mark.parametrize('acct_name', [
'',
'Assets',
'Equity',
'Expenses',
'Income',
'Liabilities',
])
def test_normalize_amount_func_bad_acct_name(acct_name):
with pytest.raises(ValueError):
core.normalize_amount_func(acct_name)
def test_sort_and_filter_accounts():
accounts = (data.Account(s) for s in [
'Expenses:Services',
'Assets:Receivable',
'Income:Other',
'Liabilities:Payable',
'Equity:Funds:Unrestricted',
'Income:Donations',
'Expenses:Other',
])
actual = core.sort_and_filter_accounts(accounts, ['Equity', 'Income', 'Expenses'])
assert list(actual) == [
(0, 'Equity:Funds:Unrestricted'),
(1, 'Income:Donations'),
(1, 'Income:Other'),
(2, 'Expenses:Other'),
(2, 'Expenses:Services'),
]
def test_sort_and_filter_accounts_unused_name():
accounts = (data.Account(s) for s in [
'Liabilities:CreditCard',
'Assets:Cash',
'Assets:Receivable:Accounts',
])
actual = core.sort_and_filter_accounts(
accounts, ['Assets:Receivable', 'Liabilities:Payable', 'Assets', 'Liabilities'],
)
assert list(actual) == [
(0, 'Assets:Receivable:Accounts'),
(2, 'Assets:Cash'),
(3, 'Liabilities:CreditCard'),
]
def test_sort_and_filter_accounts_with_subaccounts():
accounts = (data.Account(s) for s in [
'Assets:Checking',
'Assets:Receivable:Fraud',
'Assets:Cash',
'Assets:Receivable:Accounts',
])
actual = core.sort_and_filter_accounts(accounts, ['Assets:Receivable', 'Assets'])
assert list(actual) == [
(0, 'Assets:Receivable:Accounts'),
(0, 'Assets:Receivable:Fraud'),
(1, 'Assets:Cash'),
(1, 'Assets:Checking'),
]
@pytest.mark.parametrize('empty_arg', ['accounts', 'order'])
def test_sort_and_filter_accounts_empty_accounts(empty_arg):
accounts = [data.Account(s) for s in ['Expenses:Other', 'Income:Other']]
if empty_arg == 'accounts':
args = ([], accounts)
else:
args = (accounts, [])
actual = core.sort_and_filter_accounts(*args)
assert next(actual, None) is None
def check_account_balance(balance_seq, account, balance):
assert next(balance_seq, None) == (account, {'USD': testutil.Amount(balance)})
@pytest.mark.parametrize('days_after', range(4))
def test_account_balances(balance_postings, days_after):
start_date = testutil.FY_MID_DATE + datetime.timedelta(days=days_after)
balance_cls = core.PeriodPostings.with_start_date(start_date)
groups = dict(balance_cls.group_by_account(balance_postings))
actual = core.account_balances(groups)
expect_opening = -1027
opening_acct, opening_bal = next(actual)
if days_after < 1:
check_account_balance(actual, 'Equity:OpeningBalance', -1000)
expect_opening += 1000
if days_after < 3:
check_account_balance(actual, 'Equity:Realized:CurrencyConversion', 1)
expect_opening -= 1
if days_after < 2:
check_account_balance(actual, 'Income:Donations', -30)
expect_opening += 30
elif days_after < 3:
check_account_balance(actual, 'Income:Donations', -20)
expect_opening += 20
if days_after < 2:
check_account_balance(actual, 'Expenses:BankingFees', 1)
expect_opening -= 1
if days_after < 3:
check_account_balance(actual, 'Expenses:Services:Fundraising', 1)
expect_opening -= 1
if expect_opening:
assert opening_bal == {'USD': testutil.Amount(expect_opening)}
else:
assert opening_bal.is_zero()
assert opening_acct == core.OPENING_BALANCE_NAME
check_account_balance(actual, core.ENDING_BALANCE_NAME, -1027)
assert next(actual, None) is None
def test_account_balances_order_arg(balance_postings):
start_date = testutil.FY_MID_DATE + datetime.timedelta(days=1)
balance_cls = core.PeriodPostings.with_start_date(start_date)
groups = dict(balance_cls.group_by_account(balance_postings))
actual = core.account_balances(groups, ['Income', 'Assets'])
check_account_balance(actual, core.OPENING_BALANCE_NAME, 1000)
check_account_balance(actual, 'Income:Donations', -30)
check_account_balance(actual, 'Assets:Checking', 27)
check_account_balance(actual, core.ENDING_BALANCE_NAME, 997)
assert next(actual, None) is None
def test_account_balances_order_filters_all(balance_postings):
start_date = testutil.FY_MID_DATE + datetime.timedelta(days=1)
balance_cls = core.PeriodPostings.with_start_date(start_date)
groups = dict(balance_cls.group_by_account(balance_postings))
actual = core.account_balances(groups, ['Liabilities'])
account, balance = next(actual)
assert account is core.OPENING_BALANCE_NAME
assert balance.is_zero()
account, balance = next(actual)
assert account is core.ENDING_BALANCE_NAME
assert balance.is_zero()
def test_account_balances_empty_postings():
actual = core.account_balances({})
account, balance = next(actual)
assert account is core.OPENING_BALANCE_NAME
assert balance.is_zero()
account, balance = next(actual)
assert account is core.ENDING_BALANCE_NAME
assert balance.is_zero()