Files @ 5784068904e8
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_reports_core.py

bkuhn
payroll-type — US:403b:Employee:Roth — needed separate since taxable

Since Roth contributions are taxable, there are some reports that
need to include these amounts in total salary (i.e., when running a
report that seeks to show total taxable income for an employee). As
such, we need a `payroll-type` specifically for Roth 403(b)
contributions.
"""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()