"""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()