Files @ 5784068904e8
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_meta_entity.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.
ad268f049df6
ad268f049df6
1b7fdf4f3b00
ad268f049df6
1b7fdf4f3b00
1b7fdf4f3b00
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
499f18ff623c
ad268f049df6
499f18ff623c
499f18ff623c
ad268f049df6
499f18ff623c
499f18ff623c
ad268f049df6
499f18ff623c
ad268f049df6
499f18ff623c
499f18ff623c
499f18ff623c
499f18ff623c
499f18ff623c
499f18ff623c
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
499f18ff623c
56b644f1db9b
56b644f1db9b
499f18ff623c
499f18ff623c
499f18ff623c
ad268f049df6
ad268f049df6
ad268f049df6
499f18ff623c
ad268f049df6
499f18ff623c
499f18ff623c
499f18ff623c
3e20b863e07e
3e20b863e07e
3e20b863e07e
ad268f049df6
499f18ff623c
3e20b863e07e
3e20b863e07e
56b644f1db9b
3e20b863e07e
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
ad268f049df6
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
2cb131423f97
ad268f049df6
ad268f049df6
ad268f049df6
8bc17dbf4a9a
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
8bc17dbf4a9a
8bc17dbf4a9a
ad268f049df6
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
2cb131423f97
ad268f049df6
ad268f049df6
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
8bc17dbf4a9a
8bc17dbf4a9a
8bc17dbf4a9a
0413fed8b957
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
56b644f1db9b
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
0413fed8b957
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
71f50a6cf864
ad268f049df6
c712105bed3c
ad268f049df6
c712105bed3c
c712105bed3c
3a4c8526b2b2
ad268f049df6
ad268f049df6
ad268f049df6
c712105bed3c
c712105bed3c
c712105bed3c
ad268f049df6
ad268f049df6
ad268f049df6
c712105bed3c
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
ad268f049df6
9f0c30738db8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
51eee8ec8fd8
9f0c30738db8
701ccdc19250
9f0c30738db8
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
b8d76ec5a0a3
552ef45f47df
552ef45f47df
552ef45f47df
552ef45f47df
552ef45f47df
552ef45f47df
552ef45f47df
"""Test validation of entity 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.

import pytest

from . import testutil

from conservancy_beancount.plugin import meta_entity

VALID_VALUES = {
    # Classic entity: LastName-FirstName
    'Smith-Alex',
    # Various people and companies have one-word names
    # Digits are allowed, as part of a name or standalone
    'Company19',
    'Company-19',
    # No case requirements
    'boyd-danah',
    # No limit on the number of parts of the name
    'B-van-der-A',
    # Names that have no ASCII are allowed, with or without dash separators
    '田中流星',
    '田中-流星',
    'スミスダコタ',
    'スミス-ダコタ',
    'Яшин-Данила',
    # Governments, using : as a hierarchy separator
    'BE',
    'US:KY',
    'CA:ON',
    # The PayPal importer allows ASCII punctuation in entity metadata
    'Du-Bois-W.-E.-B.',
    "O'Malley-Thomas",
    'O`Malley-Thomas',
    # import2ledger produces entities that end with -
    # That's probably a bug, but allow it for now.
    'foo-',
}

INVALID_VALUES = {
    # Starting with a - is not allowed
    '-foo',
    # Names that can be reduced to ASCII should be
    # Producers should change this to Uberentity or Ueberentity
    # I am not wild about this rule and would like to relax it—it's mostly
    # based on an expectation that entities are typed in by an American. That's
    # true less and less and it seems like we should reduce the amount of
    # mangling producers are expected to do. But it's the rule for today.
    'Überentity',
    # Whitespace is never allowed
    'Alex Smith',
    '田中\u00A0流星',  # Non-breaking space
    # Non-ASCII punctuation is not allowed
    'Яшин—Данила',  # em dash
    'O’Malley-Thomas',  # Right-angled apostrophe
    'Du-Bois-W。-E。-B。',  # Japanese period
}

ANONYMOUS_VALUES = {
    # Values produced by various importers that should be translated to
    # Anonymous.
    '',
    ' ',
    '-',
    '--',
    '-----',
    '_',
    ' _ ',
    '.',
}

TEST_KEY = 'entity'

@pytest.fixture(scope='module')
def hook():
    config = testutil.TestConfig()
    return meta_entity.MetaEntity(config)

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

@pytest.mark.parametrize('src_value', ANONYMOUS_VALUES)
def test_anonymous_values_on_postings(hook, src_value):
    txn = testutil.Transaction(postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25, {TEST_KEY: src_value}),
    ])
    assert not any(hook.run(txn))
    assert txn.postings[-1].meta[TEST_KEY] == 'Anonymous'

@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 len(errors) == 1
    assert errors[0].message == "Expenses:General has invalid entity: {}".format(src_value)

@pytest.mark.parametrize('src_value', VALID_VALUES)
def test_valid_values_on_transactions(hook, src_value):
    txn = testutil.Transaction(payee='Payee', **{TEST_KEY: src_value}, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    assert not any(hook.run(txn))
    # Make sure payee doesn't overwrite metadata. See payee test below.
    assert txn.meta[TEST_KEY] == src_value

@pytest.mark.parametrize('src_value', ANONYMOUS_VALUES)
def test_anonymous_values_on_transactions(hook, src_value):
    txn = testutil.Transaction(**{TEST_KEY: src_value}, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    assert not any(hook.run(txn))
    assert txn.meta[TEST_KEY] == 'Anonymous'

@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 1 <= len(errors) <= 2
    assert all(error.message == "transaction has invalid entity: {}".format(src_value)
               for error in hook.run(txn))

@pytest.mark.parametrize('src_value', VALID_VALUES)
def test_valid_values_on_payee(hook, src_value):
    txn = testutil.Transaction(payee=src_value, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    assert not any(hook.run(txn))
    # In this case, we want the hook to set metadata to make it easier to
    # write bean-queries.
    assert txn.meta[TEST_KEY] == src_value

@pytest.mark.parametrize('src_value', ANONYMOUS_VALUES)
def test_anonymous_values_on_payee(hook, src_value):
    txn = testutil.Transaction(payee=src_value, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    assert not any(hook.run(txn))
    assert txn.meta[TEST_KEY] == 'Anonymous'

@pytest.mark.parametrize('src_value', INVALID_VALUES)
def test_invalid_values_on_payee(hook, src_value):
    txn = testutil.Transaction(payee=src_value, postings=[
        ('Assets:Cash', -25),
        ('Expenses:General', 25),
    ])
    errors = list(hook.run(txn))
    assert 1 <= len(errors) <= 2
    assert all(error.message == "transaction has invalid entity: {}".format(src_value)
               for error in hook.run(txn))

@pytest.mark.parametrize('payee,src_value', testutil.combine_values(
    INVALID_VALUES,
    VALID_VALUES,
))
def test_invalid_payee_but_valid_metadata(hook, payee, src_value):
    txn = testutil.Transaction(**{'payee': payee, TEST_KEY: src_value}, postings=[
        ('Assets:Cash', -25),
        ('Expenses:Other', 25),
    ])
    assert not any(hook.run(txn))

def test_mixed_sources(hook):
    txn = testutil.Transaction(payee='Payee', postings=[
        ('Income:Donations', -5),
        ('Equity:Funds:Restricted', 5, {TEST_KEY: 'Entity'}),
    ])
    assert not any(hook.run(txn))
    assert txn.postings[-1].meta[TEST_KEY] == 'Entity'
    assert txn.meta[TEST_KEY] == 'Payee'
    try:
        assert txn.postings[0].meta[TEST_KEY] == 'Payee'
    except (KeyError, TypeError):
        pass

@pytest.mark.parametrize('account,required', [
    ('Assets:Bank:Checking', False),
    ('Assets:Cash', False),
    ('Assets:Receivable:Accounts', True),
    ('Assets:Receivable:Loans', True),
    ('Equity:OpeningBalances', False),
    ('Expenses:General', True),
    ('Income:Donations', True),
    ('Liabilities:CreditCard', False),
    ('Liabilities:Payable:Accounts', True),
    ('Liabilities:Payable:Vacation', True),
    ('Liabilities:UnearnedIncome:Donations', False),
])
def test_which_accounts_required_on(hook, account, required):
    txn = testutil.Transaction(postings=[
        ('Assets:Checking', -25),
        (account, 25),
    ])
    errors = list(hook.run(txn))
    if not required:
        assert not errors
    else:
        assert errors
        assert any(error.message == "{} missing entity".format(account)
                   for error in errors)

def test_dont_set_entity_none(hook):
    txn = testutil.Transaction(postings=[
        ('Expenses:Other', 5),
        ('Assets:Cash', -5),
    ])
    assert any(hook.run(txn))
    assert 'entity' not in txn.meta
    for post in txn.postings:
        assert post.meta is None or 'entity' not in post.meta

def test_not_required_on_opening(hook):
    txn = testutil.OpeningBalance()
    assert not list(hook.run(txn))

@pytest.mark.parametrize('date,need_value', [
    (testutil.EXTREME_FUTURE_DATE, False),
    (testutil.FUTURE_DATE, True),
    (testutil.FY_START_DATE, True),
    (testutil.FY_MID_DATE, True),
    (testutil.PAST_DATE, False),
])
def test_required_by_date(hook, date, need_value):
    txn = testutil.Transaction(date=date, postings=[
        ('Income:Donations', -10),
        ('Assets:Checking', 10),
    ])
    assert any(hook.run(txn)) == need_value

def test_still_required_on_flagged(hook):
    txn = testutil.Transaction(flag='!', postings=[
        ('Income:Donations', -10),
        ('Assets:Checking', 10),
    ])
    assert list(hook.run(txn))