Files @ 5784068904e8
Branch filter:

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

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.
e26dffa21428
e26dffa21428
1b7fdf4f3b00
e26dffa21428
1b7fdf4f3b00
1b7fdf4f3b00
e26dffa21428
5e9e11923e8c
5e9e11923e8c
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
5e9e11923e8c
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
e26dffa21428
7a9bc2da5040
7a9bc2da5040
5e9e11923e8c
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
5e9e11923e8c
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
5e9e11923e8c
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
5e9e11923e8c
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
7a9bc2da5040
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
5e9e11923e8c
"""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()