"""Test Account class""" # Copyright © 2020 Brett Smith # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import pytest from . import testutil from datetime import date as Date from beancount.core.data import Open, Close, Booking from beancount.parser import options as bc_options from conservancy_beancount import data clean_account_meta = pytest.fixture()(testutil.clean_account_meta) @pytest.fixture def asset_hierarchy(): entries = [ Open({'classification': 'Investment'}, Date(2002, 2, 1), 'Assets:Bank:CD', None, None), Open({'classification': 'Cash'}, Date(2002, 2, 1), 'Assets:Bank:Checking', None, None), Open({'classification': 'Cash'}, Date(2002, 2, 1), 'Assets:Bank:Savings', None, None), Open({'classification': 'Cash'}, Date(2002, 2, 1), 'Assets:Cash', None, None), Open({'classification': 'Investment'}, Date(2002, 2, 1), 'Assets:Investment:Commodities', None, None), Open({'classification': 'Investment'}, Date(2002, 2, 1), 'Assets:Investment:Stocks', None, None), ] data.Account.load_openings_and_closings(entries) yield from testutil.clean_account_meta() def check_account_meta(acct_meta, opening, closing=None): if isinstance(acct_meta, str): acct_meta = data.Account(acct_meta).meta assert acct_meta == opening.meta assert acct_meta.account == opening.account assert acct_meta.booking == opening.booking assert acct_meta.currencies == opening.currencies assert acct_meta.open_date == opening.date assert acct_meta.open_meta == opening.meta if closing is None: assert acct_meta.close_date is None assert acct_meta.close_meta is None else: assert acct_meta.close_date == closing.date assert acct_meta.close_meta == closing.meta @pytest.mark.parametrize('acct_name,under_arg,expected', [ ('Expenses:Tax:Sales', 'Expenses:Tax:Sales:', False), ('Expenses:Tax:Sales', 'Expenses:Tax:Sales', True), ('Expenses:Tax:Sales', 'Expenses:Tax:', True), ('Expenses:Tax:Sales', 'Expenses:Tax', True), ('Expenses:Tax:Sales', 'Expenses:', True), ('Expenses:Tax:Sales', 'Expenses', True), ('Expenses:Tax:Sales', 'Expense', False), ('Expenses:Tax:Sales', 'Equity:', False), ('Expenses:Tax:Sales', 'Equity', False), ]) def test_is_under_one_arg(acct_name, under_arg, expected): expected = under_arg if expected else None assert data.Account(acct_name).is_under(under_arg) == expected @pytest.mark.parametrize('acct_name,expected', [ ('Assets:Cash', None), ('Assets:Checking', None), ('Assets:Prepaid:Expenses', 'Assets:Prepaid'), ('Assets:Receivable:Accounts', 'Assets:Receivable'), ]) def test_is_under_multi_arg(acct_name, expected): assert expected == data.Account(acct_name).is_under( 'Assets:Prepaid', 'Assets:Receivable', ) if expected: expected += ':' assert expected == data.Account(acct_name).is_under( 'Assets:Prepaid:', 'Assets:Receivable:', ) @pytest.mark.parametrize('acct_name,expected', [ ('Assets:Bank:Checking', True), ('Assets:Cash', True), ('Assets:Cash:EUR', True), ('Assets:Prepaid:Expenses', False), ('Assets:Prepaid:Vacation', False), ('Assets:Receivable:Accounts', False), ('Assets:Receivable:Fraud', False), ('Expenses:Other', False), ('Equity:OpeningBalance', False), ('Income:Other', False), ('Liabilities:CreditCard', False), ]) def test_is_cash_equivalent(acct_name, expected): assert data.Account(acct_name).is_cash_equivalent() == expected @pytest.mark.parametrize('acct_name,expected', [ ('Assets:Bank:Check9999', True), ('Assets:Bank:CheckCard', True), ('Assets:Bank:Checking', True), ('Assets:Bank:Savings', False), ('Assets:Cash', False), ('Assets:Check9999', True), ('Assets:CheckCard', True), ('Assets:Checking', True), ('Assets:Prepaid:Expenses', False), ('Assets:Receivable:Accounts', False), ('Expenses:Other', False), ('Equity:OpeningBalance', False), ('Income:Other', False), ('Liabilities:CreditCard', False), ]) def test_is_checking(acct_name, expected): assert data.Account(acct_name).is_checking() == expected @pytest.mark.parametrize('acct_name,expected', [ ('Assets:Cash', False), ('Assets:Prepaid:Expenses', False), ('Assets:Receivable:Accounts', False), ('Expenses:Other', False), ('Equity:OpeningBalance', False), ('Income:Other', False), ('Liabilities:CreditCard', True), ('Liabilities:CreditCard:Visa', True), ('Liabilities:Payable:Accounts', False), ('Liabilities:UnearnedIncome:Donations', False), ]) def test_is_credit_card(acct_name, expected): assert data.Account(acct_name).is_credit_card() == expected @pytest.mark.parametrize('acct_name,expected', [ ('Assets:Cash', False), ('Assets:Prepaid:Expenses', False), ('Assets:Receivable:Accounts', False), ('Expenses:Other', False), ('Equity:Funds:Restricted', True), ('Equity:Funds:Unrestricted', True), ('Equity:OpeningBalance', True), ('Equity:Retained:Costs', False), ('Income:Other', False), ('Liabilities:CreditCard', False), ('Liabilities:Payable:Accounts', False), ('Liabilities:UnearnedIncome:Donations', False), ]) def test_is_opening_equity(acct_name, expected): assert data.Account(acct_name).is_opening_equity() == expected @pytest.mark.parametrize('acct_name', [ 'Assets:Cash', 'Assets:Receivable:Accounts', 'Expenses:Other', 'Equity:Funds:Restricted', 'Income:Other', 'Liabilities:CreditCard', 'Liabilities:Payable:Accounts', ]) def test_slice_parts_no_args(acct_name): account = data.Account(acct_name) assert account.slice_parts() == acct_name.split(':') @pytest.mark.parametrize('acct_name', [ 'Assets:Cash', 'Assets:Receivable:Accounts', 'Expenses:Other', 'Equity:Funds:Restricted', 'Income:Other', 'Liabilities:CreditCard', 'Liabilities:Payable:Accounts', ]) def test_slice_parts_index(acct_name): account = data.Account(acct_name) parts = acct_name.split(':') for index, expected in enumerate(parts): assert account.slice_parts(index) == expected with pytest.raises(IndexError): account.slice_parts(index + 1) @pytest.mark.parametrize('acct_name', [ 'Assets:Cash', 'Assets:Receivable:Accounts', 'Expenses:Other', 'Equity:Funds:Restricted', 'Income:Other', 'Liabilities:CreditCard', 'Liabilities:Payable:Accounts', ]) def test_slice_parts_range(acct_name): account = data.Account(acct_name) parts = acct_name.split(':') for start, stop in zip([0, 0, 1, 1], [2, 3, 2, 3]): assert account.slice_parts(start, stop) == parts[start:stop] @pytest.mark.parametrize('acct_name', [ 'Assets:Cash', 'Assets:Receivable:Accounts', 'Expenses:Other', 'Equity:Funds:Restricted', 'Income:Other', 'Liabilities:CreditCard', 'Liabilities:Payable:Accounts', ]) def test_slice_parts_slice(acct_name): account = data.Account(acct_name) parts = acct_name.split(':') for start, stop in zip([0, 0, 1, 1], [2, 3, 2, 3]): sl = slice(start, stop) assert account.slice_parts(sl) == parts[start:stop] @pytest.mark.parametrize('acct_name', [ 'Assets:Cash', 'Assets:Receivable:Accounts', 'Expenses:Other', 'Equity:Funds:Restricted', 'Income:Other', 'Liabilities:CreditCard', 'Liabilities:Payable:Accounts', ]) def test_count_parts(acct_name): account = data.Account(acct_name) assert account.count_parts() == acct_name.count(':') + 1 @pytest.mark.parametrize('acct_name', [ 'Assets:Cash', 'Assets:Receivable:Accounts', 'Expenses:Other', 'Equity:Funds:Restricted', 'Income:Other', 'Liabilities:CreditCard', 'Liabilities:Payable:Accounts', ]) def test_root_part(acct_name): account = data.Account(acct_name) parts = acct_name.split(':') assert account.root_part() == parts[0] assert account.root_part(1) == parts[0] assert account.root_part(2) == ':'.join(parts[:2]) def test_load_opening(clean_account_meta): opening = Open({'lineno': 210}, Date(2010, 2, 1), 'Assets:Cash', None, None) data.Account.load_opening(opening) check_account_meta('Assets:Cash', opening) def test_load_closing(clean_account_meta): name = 'Assets:Checking' opening = Open({'lineno': 230}, Date(2010, 10, 1), name, None, None) closing = Close({'lineno': 235}, Date(2010, 11, 1), name) data.Account.load_opening(opening) data.Account.load_closing(closing) check_account_meta(name, opening, closing) def test_load_closing_without_opening(clean_account_meta): closing = Close({'lineno': 245}, Date(2010, 3, 1), 'Assets:Cash') with pytest.raises(ValueError): data.Account.load_closing(closing) def test_load_openings_and_closings(clean_account_meta): entries = [ Open({'lineno': 1, 'income-type': 'Donations'}, Date(2000, 3, 1), 'Income:Donations', None, None), Open({'lineno': 2}, Date(2000, 3, 1), 'Income:Other', None, None), Open({'lineno': 3, 'asset-type': 'Cash equivalent'}, Date(2000, 4, 1), 'Assets:Checking', ['USD', 'EUR'], Booking.STRICT), testutil.Transaction(date=Date(2000, 4, 10), postings=[ ('Income:Donations', -10), ('Assets:Checking', 10), ]), Close({'lineno': 30, 'why': 'Changed banks'}, Date(2000, 5, 1), 'Assets:Checking') ] data.Account.load_openings_and_closings(iter(entries)) check_account_meta('Income:Donations', entries[0]) check_account_meta('Income:Other', entries[1]) check_account_meta('Assets:Checking', entries[2], entries[-1]) @pytest.mark.parametrize('account_s', [ 'Assets:Bank:Checking', 'Equity:Funds:Restricted', 'Expenses:Other', 'Income:Donations', 'Liabilities:CreditCard:Visa', ]) def test_is_account(account_s): assert data.Account.is_account(account_s) @pytest.mark.parametrize('account_s', [ 'Assets:Bank:12-345', 'Equity:Funds:Restricted', 'Expenses:Other', 'Income:Donations', 'Liabilities:CreditCard:Visa0123', ]) def test_is_account(account_s): assert data.Account.is_account(account_s) @pytest.mark.parametrize('account_s', [ 'Assets:checking', 'Assets::Cash', 'Equity', 'Liabilities:Credit Card', 'income:Donations', 'Expenses:Banking_Fees', 'Revenue:Grants', ]) def test_is_not_account(account_s): assert not data.Account.is_account(account_s) @pytest.mark.parametrize('account_s,expected', [ ('Revenue:Donations', True), ('Costs:Other', True), ('Income:Donations', False), ('Expenses:Other', False), ]) def test_is_account_respects_configured_roots(clean_account_meta, account_s, expected): config = bc_options.OPTIONS_DEFAULTS.copy() config['name_expenses'] = 'Costs' config['name_income'] = 'Revenue' data.Account.load_options_map(config) assert data.Account.is_account(account_s) == expected def test_load_from_books(clean_account_meta): entries = [ Open({'lineno': 310}, Date(2001, 1, 1), 'Assets:Bank:Checking', ['USD'], None), Open({'lineno': 315}, Date(2001, 2, 1), 'Revenue:Donations', None, Booking.STRICT), testutil.Transaction(date=Date(2001, 2, 10), postings=[ ('Revenue:Donations', -10), ('Assets:Bank:Checking', 10), ]), Close({'lineno': 320}, Date(2001, 3, 1), 'Assets:Bank:Checking'), ] config = bc_options.OPTIONS_DEFAULTS.copy() config['name_expenses'] = 'Costs' config['name_income'] = 'Revenue' data.Account.load_from_books(entries, config) for post in entries[2].postings: assert data.Account.is_account(post.account) check_meta = data.Account(entries[0].account).meta assert check_meta.open_date == entries[0].date assert check_meta.close_date == entries[-1].date @pytest.mark.parametrize('arg,expect_subaccts', [ ('Assets', ['Bank:CD', 'Bank:Checking', 'Bank:Savings', 'Cash', 'Investment:Commodities', 'Investment:Stocks']), ('Assets:Bank', ['CD', 'Checking', 'Savings']), ('Assets:Investment', ['Commodities', 'Stocks']), ('Equity', []), ]) def test_iter_accounts_by_hierarchy(asset_hierarchy, arg, expect_subaccts): assert set(data.Account.iter_accounts_by_hierarchy(arg)) == { f'{arg}:{sub}' for sub in expect_subaccts } @pytest.mark.parametrize('arg,expect_subaccts', [ ('Cash', ['Bank:Checking', 'Bank:Savings', 'Cash']), ('Investment', ['Bank:CD', 'Investment:Commodities', 'Investment:Stocks']), ('Equity', []), ]) def test_iter_accounts_by_classification(asset_hierarchy, arg, expect_subaccts): assert set(data.Account.iter_accounts_by_classification(arg)) == { f'Assets:{sub}' for sub in expect_subaccts } @pytest.mark.parametrize('arg,expect_subaccts', [ ('Assets', ['Bank:CD', 'Bank:Checking', 'Bank:Savings', 'Cash', 'Investment:Commodities', 'Investment:Stocks']), ('Assets:Bank', ['CD', 'Checking', 'Savings']), ('Assets:Investment', ['Commodities', 'Stocks']), ('Cash', ['Bank:Checking', 'Bank:Savings', 'Cash']), ('Investment', ['Bank:CD', 'Investment:Commodities', 'Investment:Stocks']), ('Equity', []), ('Unused classification', []), ]) def test_iter_accounts(asset_hierarchy, arg, expect_subaccts): if arg.startswith('Assets'): prefix = arg else: prefix = 'Assets' assert set(data.Account.iter_accounts(arg)) == { f'{prefix}:{sub}' for sub in expect_subaccts }