Files @ 5784068904e8
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_meta_project.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 handling of project metadata"""
# 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.

from pathlib import Path

import pytest

from . import testutil

from conservancy_beancount import errors as errormod
from conservancy_beancount.plugin import meta_project

VALID_VALUES = {
    'Conservancy': 'Conservancy',
    'Alpha': 'Alpha',
    'Bravo': 'Bravo',
    'Charles': 'Charlie',
    'Chuck': 'Charlie',
}

INVALID_VALUES = {
    'Alhpa',
    'Yankee',
    '',
}

TEST_KEY = 'project'
DEFAULT_VALUE = 'Conservancy'

@pytest.fixture(scope='module')
def hook():
    config = testutil.TestConfig(repo_path='repository')
    return meta_project.MetaProject(config)

@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
def test_valid_values_on_postings(hook, src_value, set_value):
    txn = testutil.Transaction(postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25, {TEST_KEY: src_value}),
    ])
    errors = list(hook.run(txn))
    assert not errors
    testutil.check_post_meta(txn, None, {TEST_KEY: set_value})

@pytest.mark.parametrize('src_value', INVALID_VALUES)
def test_invalid_values_on_postings(hook, src_value):
    txn = testutil.Transaction(postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25, {TEST_KEY: src_value}),
    ])
    errors = list(hook.run(txn))
    assert errors
    testutil.check_post_meta(txn, None, {TEST_KEY: src_value})

@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
def test_valid_values_on_transactions(hook, src_value, set_value):
    txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    errors = list(hook.run(txn))
    assert not errors
    testutil.check_post_meta(txn, None, {TEST_KEY: set_value})

@pytest.mark.parametrize('src_value', INVALID_VALUES)
def test_invalid_values_on_transactions(hook, src_value):
    txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    errors = list(hook.run(txn))
    assert errors
    testutil.check_post_meta(txn, None, None)

@pytest.mark.parametrize('account,required', [
    ('Assets:Cash', False),
    ('Assets:Receivable:Accounts', True),
    ('Assets:Receivable:Loans', True),
    ('Equity:OpeningBalance', False),
    ('Equity:Funds:Restricted', True),
    ('Equity:Funds:Unrestricted', False),
    ('Expenses:General', True),
    ('Income:Donations', True),
    ('Liabilities:CreditCard', False),
    ('Liabilities:Payable:Accounts', True),
    # We do want a "project" for Lia:Pay:Vacation but it has a default value
    ('Liabilities:Payable:Vacation', False),
    ('Liabilities:UnearnedIncome:Donations', True),
])
def test_which_accounts_required_on(hook, account, required):
    txn = testutil.Transaction(postings=[
        ('Assets:Checking', 25),
        (account, 25),
    ])
    errors = list(hook.run(txn))
    assert required == any(errors)

@pytest.mark.parametrize('account', [
    'Equity:Funds:Unrestricted',
    'Equity:Realized:CurrencyConversion',
    'Expenses:Payroll:Salary',
    'Expenses:Payroll:Tax',
    'Liabilities:Payable:Vacation',
])
def test_default_values(hook, account):
    txn = testutil.Transaction(postings=[
        ('Assets:Checking', -25),
        (account, 25),
    ])
    errors = list(hook.run(txn))
    assert not errors
    testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE})

@pytest.mark.parametrize('equity,other_acct,value', testutil.combine_values(
    ['Equity:Funds:Unrestricted', 'Equity:Realized:CurrencyConversion'],
    ['Assets:Checking', 'Liabilities:CreditCard'],
    VALID_VALUES,
))
def test_equity_override_txn_meta(hook, equity, other_acct, value):
    if value == DEFAULT_VALUE:
        value = f'Not{value}'
    txn = testutil.Transaction(**{TEST_KEY: value}, postings=[
        (other_acct, 100),
        (equity, -100),
    ])
    errors = list(hook.run(txn))
    assert not errors
    testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE})

@pytest.mark.parametrize('equity,other_acct,value', testutil.combine_values(
    ['Equity:Funds:Unrestricted', 'Equity:Realized:CurrencyConversion'],
    ['Assets:Checking', 'Liabilities:CreditCard'],
    VALID_VALUES,
))
def test_equity_override_post_meta(hook, equity, other_acct, value):
    if value == DEFAULT_VALUE:
        value = f'Not{value}'
    txn = testutil.Transaction(postings=[
        (other_acct, 100),
        (equity, -100, {TEST_KEY: value}),
    ])
    actual = {error.message for error in hook.run(txn)}
    assert actual == {f"{equity} has invalid {TEST_KEY}: {value}"}
    testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE})

@pytest.mark.parametrize('date,required', [
    (testutil.EXTREME_FUTURE_DATE, False),
    (testutil.FUTURE_DATE, True),
    (testutil.FY_START_DATE, True),
    (testutil.FY_MID_DATE, True),
    (testutil.PAST_DATE, None),
])
def test_default_value_set_in_date_range(hook, date, required):
    txn = testutil.Transaction(date=date, postings=[
        ('Expenses:Payroll:Benefits', 25),
        ('Liabilities:Payable:Vacation', -25),
    ])
    errors = list(hook.run(txn))
    assert not errors
    expect_meta = {TEST_KEY: DEFAULT_VALUE} if required else None
    testutil.check_post_meta(txn, expect_meta, expect_meta)

@pytest.mark.parametrize('repo_path', [
    None,
    '..',
])
def test_missing_project_data(repo_path):
    config = testutil.TestConfig(repo_path=repo_path)
    with pytest.raises(errormod.ConfigurationError):
        meta_project.MetaProject(config)

@pytest.mark.parametrize('repo_path_s,data_path_s', [
    ('repository', 'Projects/project-list.yml'),
    ('..', 'LICENSE.txt'),
])
def test_invalid_project_data(repo_path_s, data_path_s):
    config = testutil.TestConfig(repo_path=repo_path_s)
    with pytest.raises(errormod.ConfigurationError):
        meta_project.MetaProject(config, Path(data_path_s))

def test_not_required_on_opening(hook):
    txn = testutil.OpeningBalance('Equity:Funds:Unrestricted')
    assert not list(hook.run(txn))

def test_always_required_on_restricted_funds(hook):
    acct = 'Equity:Funds:Restricted'
    txn = testutil.OpeningBalance(acct)
    actual = {error.message for error in hook.run(txn)}
    assert actual == {f'{acct} missing project'}

@pytest.mark.parametrize('src_value', INVALID_VALUES)
def test_still_required_on_flagged_txn(hook, src_value):
    txn = testutil.Transaction(flag='!', **{TEST_KEY: src_value}, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    errors = list(hook.run(txn))
    assert errors
    testutil.check_post_meta(txn, None, None)

@pytest.mark.parametrize('src_value', testutil.FIXME_VALUES)
def test_flagged_fixme_ok(hook, src_value):
    txn = testutil.Transaction(flag='!', postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25, {TEST_KEY: src_value}),
    ])
    errors = list(hook.run(txn))
    assert not errors
    testutil.check_post_meta(txn, None, {TEST_KEY: src_value})