Changeset - 71f50a6cf864
[Not reviewed]
0 4 0
Brett Smith - 4 years ago 2020-12-29 17:20:53
brettcsmith@brettcsmith.org
data: Bugfix is_opening_balance_txn() for donations from equity. RT#13516

Opening balance transactions should only include opening equity
accounts and non-equity accounts. Reflect that in the test.
4 files changed with 21 insertions and 6 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/data.py
Show inline comments
...
 
@@ -666,31 +666,31 @@ def balance_of(txn: Transaction,
 
    number = decimal.Decimal(0)
 
    if not match_posts:
 
        currency = ''
 
    else:
 
        weights: Sequence[Amount] = [
 
            bc_convert.get_weight(post) for post in match_posts
 
        ]
 
        number = sum((wt.number for wt in weights), number)
 
        currency = weights[0].currency
 
    return Amount(number, currency)
 

	
 
_opening_balance_cache: MutableMapping[str, bool] = _SizedDict()
 
def is_opening_balance_txn(txn: Transaction) -> bool:
 
    key = '\0'.join(
 
        f'{post.account}={post.units}' for post in txn.postings
 
    )
 
    try:
 
        return _opening_balance_cache[key]
 
    except KeyError:
 
        pass
 
    opening_equity = balance_of(txn, Account.is_opening_equity)
 
    if not opening_equity.currency:
 
        retval = False
 
    else:
 
        rest = balance_of(txn, lambda acct: not acct.is_opening_equity())
 
        if not rest.currency:
 
            retval = False
 
        else:
 
            retval = abs(opening_equity.number + rest.number) < decimal.Decimal('.01')
 
        rest = balance_of(txn, lambda acct: not acct.is_under(*EQUITY_ACCOUNTS))
 
        retval = (
 
            opening_equity.currency == rest.currency
 
            and abs(opening_equity.number + rest.number) < decimal.Decimal('.01')
 
        )
 
    _opening_balance_cache[key] = retval
 
    return retval
setup.py
Show inline comments
 
#!/usr/bin/env python3
 

	
 
from setuptools import setup
 

	
 
setup(
 
    name='conservancy_beancount',
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.14.2',
 
    version='1.14.3',
 
    author='Software Freedom Conservancy',
 
    author_email='info@sfconservancy.org',
 
    license='GNU AGPLv3+',
 

	
 
    install_requires=[
 
        'babel>=2.6',  # Debian:python3-babel
 
        'beancount>=2.2',  # Debian:beancount
 
        'GitPython>=2.0',  # Debian:python3-git
 
        # 1.4.1 crashes when trying to save some documents.
 
        'odfpy>=1.4.0,!=1.4.1',  # Debian:python3-odf
 
        'PyYAML>=3.0',  # Debian:python3-yaml
 
        'regex',  # Debian:python3-regex
 
        'rt>=2.0',
 
    ],
 
    setup_requires=[
 
        'pytest-mypy',
 
        'pytest-runner',  # Debian:python3-pytest-runner
 
    ],
 
    tests_require=[
 
        'mypy>=0.770',  # Debian:python3-mypy
 
        'pytest',  # Debian:python3-pytest
 
    ],
 

	
 
    packages=[
tests/test_data_is_opening_balance_txn.py
Show inline comments
...
 
@@ -29,31 +29,33 @@ def test_typical_opening():
 
def test_multiacct_opening():
 
    txn = testutil.Transaction(postings=[
 
        ('Assets:Receivable:Accounts', 100),
 
        (next(testutil.OPENING_EQUITY_ACCOUNTS), -100),
 
        ('Liabilities:Payable:Accounts', -150),
 
        (next(testutil.OPENING_EQUITY_ACCOUNTS), 150),
 
    ])
 
    assert data.is_opening_balance_txn(txn)
 

	
 
def test_opening_with_fx():
 
    txn = testutil.OpeningBalance()
 
    equity_post = txn.postings[-1]
 
    txn.postings[-1] = equity_post._replace(
 
        units=testutil.Amount(equity_post.units.number * Decimal('.9'), 'EUR'),
 
        cost=testutil.Cost('1.11111'),
 
    )
 
    assert data.is_opening_balance_txn(txn)
 

	
 
@pytest.mark.parametrize('acct1,acct2,number', [
 
    ('Assets:Receivable:Accounts', 'Income:Donations', 100),
 
    ('Expenses:Other', 'Liabilities:Payable:Accounts', 200),
 
    ('Expenses:Other', 'Equity:Retained:Costs', 300),
 
    # Release from restriction
 
    ('Equity:Funds:Unrestricted', 'Equity:Funds:Restricted', 400),
 
    # Donation from project fund
 
    ('Equity:Funds:Restricted', 'Income:Donations', 500),
 
])
 
def test_not_opening_balance(acct1, acct2, number):
 
    txn = testutil.Transaction(postings=[
 
        (acct1, number),
 
        (acct2, -number),
 
    ])
 
    assert not data.is_opening_balance_txn(txn)
tests/test_meta_entity.py
Show inline comments
...
 
@@ -167,48 +167,61 @@ def test_anonymous_values_on_payee(hook, src_value):
 
    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)
0 comments (0 inline, 0 general)