Changeset - fe3560b748fa
[Not reviewed]
0 3 2
Brett Smith - 4 years ago 2021-02-11 18:38:11
brettcsmith@brettcsmith.org
meta_tax_reporting: New plugin validation.
5 files changed with 247 insertions and 7 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/data.py
Show inline comments
...
 
@@ -76,2 +76,3 @@ LINK_METADATA = frozenset([
 
    'statement',
 
    'tax-reporting',
 
    'tax-statement',
conservancy_beancount/plugin/__init__.py
Show inline comments
...
 
@@ -54,8 +54,2 @@ class HookRegistry:
 
        '.meta_paypal_id': ['MetaPayPalID'],
 
        '.meta_project': None,
 
        '.meta_receipt': None,
 
        '.meta_receivable_documentation': None,
 
        '.meta_repo_links': None,
 
        '.meta_rt_links': ['MetaRTLinks'],
 
        '.meta_tax_implication': None,
 
        '.meta_payroll_type': [
...
 
@@ -66,2 +60,9 @@ class HookRegistry:
 
        ],
 
        '.meta_project': None,
 
        '.meta_receipt': None,
 
        '.meta_receivable_documentation': None,
 
        '.meta_repo_links': None,
 
        '.meta_rt_links': ['MetaRTLinks'],
 
        '.meta_tax_implication': None,
 
        '.meta_tax_reporting': None,
 
        '.txn_date': ['TransactionDate'],
conservancy_beancount/plugin/meta_tax_reporting.py
Show inline comments
 
new file 100644
 
"""meta_tax_reporting - Validate tax-reporting metadata links"""
 
# Copyright © 2021  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
 

	
 
from . import core
 
from .. import config as configmod
 
from .. import data
 
from .. import errors as errormod
 
from ..beancount_types import (
 
    Transaction,
 
)
 

	
 
from .meta_tax_implication import MetaTaxImplication
 
from ..ranges import DateRange
 

	
 
class MetaTaxReporting(core._RequireLinksPostingMetadataHook):
 
    CHECKED_IMPLICATIONS = frozenset(
 
        # We load values through the MetadataEnum to future-proof against
 
        # changes to tax-implication. This ensures that the set contains
 
        # canonical values, or else this code will crash if canonical values
 
        # can't be found.
 
        MetaTaxImplication.VALUES_ENUM[value] for value in [
 
            '1099-MISC-Other',
 
            '1099-NEC',
 
            'Foreign-Grantee',
 
            'Foreign-Individual-Contractor',
 
            'USA-501c3',
 
            'USA-Grantee',
 
        ])
 
    CHECKED_METADATA = ['tax-reporting']
 
    SKIP_FLAGS = '!'
 
    TXN_DATE_RANGE = DateRange(datetime.date(2020, 3, 1), datetime.date.max)
 

	
 
    def __init__(self, config: configmod.Config) -> None:
 
        self._implication_hook = MetaTaxImplication(config)
 
        # Yes, we create our own MetaTaxImplication hook. This is a little
 
        # weird but it does two things for us:
 
        # 1. We can check MetaTaxImplication._run_on_post() as part of our own
 
        # implementation without duplicating the logic.
 
        # 2. We can canonicalize values through the hook. We don't strictly
 
        # need an instance for that, but we have it anyway so doing it this way
 
        # is nicer.
 

	
 
    def _run_on_post(self, txn: Transaction, post: data.Posting) -> bool:
 
        if not self._implication_hook._run_on_post(txn, post):
 
            return False
 
        implication = str(post.meta.get('tax-implication') or '')
 
        normalized = self._implication_hook.VALUES_ENUM.get(implication)
 
        return normalized in self.CHECKED_IMPLICATIONS
setup.py
Show inline comments
...
 
@@ -7,3 +7,3 @@ setup(
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.17.1',
 
    version='1.18.0',
 
    author='Software Freedom Conservancy',
tests/test_meta_tax_reporting.py
Show inline comments
 
new file 100644
 
"""test_meta_tax_reporting.py - Unit tests for tax-reporting metadata validation"""
 
# Copyright © 2021  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_tax_reporting
 

	
 
TEST_KEY = 'tax-reporting'
 
IMPLICATION_KEY = 'tax-implication'
 

	
 
REQUIRED_ACCOUNTS = {
 
    'Assets:Checking',
 
    'Assets:Bank:Savings',
 
}
 

	
 
NON_REQUIRED_ACCOUNTS = {
 
    'Assets:Prepaid:Expenses',
 
    'Assets:Receivable:Accounts',
 
    'Liabilities:CreditCard',
 
}
 

	
 
REQUIRED_AMOUNTS = {-50, -500}
 
NON_REQUIRED_AMOUNTS = {-5, 500}
 

	
 
REQUIRED_IMPLICATIONS = {
 
    '1099',
 
    '1099-Misc-Other',
 
    'foreign-grantee',
 
    'Foreign-Individual-Contractor',
 
    'USA-501c3',
 
    'US-Grantee',
 
}
 

	
 
NON_REQUIRED_IMPLICATIONS = {
 
    'Bank-Transfer',
 
    'chargeback',
 
    'Foreign-Corp',
 
    'Loan',
 
    'refund',
 
    'Reimbursement',
 
    'retirement-pretax',
 
    'Tax-Payment',
 
    'us-corp',
 
    'w2',
 
}
 

	
 
@pytest.fixture(scope='module')
 
def hook():
 
    config = testutil.TestConfig(payment_threshold=10)
 
    return meta_tax_reporting.MetaTaxReporting(config)
 

	
 
@pytest.mark.parametrize('account,amount,implication,value', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
    testutil.LINK_METADATA_STRINGS,
 
))
 
def test_pass_on_txn(hook, account, amount, implication, value):
 
    txn_meta = {
 
        IMPLICATION_KEY: implication,
 
        TEST_KEY: value,
 
    }
 
    txn = testutil.Transaction(**txn_meta, postings=[
 
        (account, amount),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert not list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication,value', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
    testutil.LINK_METADATA_STRINGS,
 
))
 
def test_pass_on_post(hook, account, amount, implication, value):
 
    post_meta = {
 
        IMPLICATION_KEY: implication,
 
        TEST_KEY: value,
 
    }
 
    txn = testutil.Transaction(postings=[
 
        (account, amount, post_meta),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert not list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
))
 
def test_error_when_missing(hook, account, amount, implication):
 
    txn = testutil.Transaction(postings=[
 
        (account, amount, {IMPLICATION_KEY: implication}),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication,value', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
    testutil.NON_LINK_METADATA_STRINGS,
 
))
 
def test_error_when_empty(hook, account, amount, implication, value):
 
    txn_meta = {
 
        IMPLICATION_KEY: implication,
 
        TEST_KEY: value,
 
    }
 
    txn = testutil.Transaction(**txn_meta, postings=[
 
        (account, amount),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication,value', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
    testutil.NON_STRING_METADATA_VALUES,
 
))
 
def test_error_when_wrong_type(hook, account, amount, implication, value):
 
    txn_meta = {
 
        IMPLICATION_KEY: implication,
 
        TEST_KEY: value,
 
    }
 
    txn = testutil.Transaction(**txn_meta, postings=[
 
        (account, amount),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication', testutil.combine_values(
 
    NON_REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
))
 
def test_skip_by_account(hook, account, amount, implication):
 
    txn = testutil.Transaction(postings=[
 
        (account, amount, {IMPLICATION_KEY: implication}),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert not list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    NON_REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
))
 
def test_skip_by_amount(hook, account, amount, implication):
 
    txn = testutil.Transaction(postings=[
 
        (account, amount, {IMPLICATION_KEY: implication}),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert not list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    NON_REQUIRED_IMPLICATIONS,
 
))
 
def test_skip_by_implication(hook, account, amount, implication):
 
    txn = testutil.Transaction(postings=[
 
        (account, amount, {IMPLICATION_KEY: implication}),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert not list(hook.run(txn))
 

	
 
@pytest.mark.parametrize('account,amount,implication', testutil.combine_values(
 
    REQUIRED_ACCOUNTS,
 
    REQUIRED_AMOUNTS,
 
    REQUIRED_IMPLICATIONS,
 
))
 
def test_skip_by_flag(hook, account, amount, implication):
 
    txn = testutil.Transaction(flag='!', postings=[
 
        (account, amount, {IMPLICATION_KEY: implication}),
 
        ('Expenses:Other', -amount),
 
    ])
 
    assert not list(hook.run(txn))
0 comments (0 inline, 0 general)