diff --git a/conservancy_beancount/plugin/meta_tax_implication.py b/conservancy_beancount/plugin/meta_tax_implication.py
new file mode 100644
index 0000000..fb6226d
--- /dev/null
+++ b/conservancy_beancount/plugin/meta_tax_implication.py
@@ -0,0 +1,50 @@
+"""meta_tax_implication - Validate taxImplication metadata"""
+# Copyright © 2020 Brett Smith
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import decimal
+
+from . import core
+
+DEFAULT_STOP_AMOUNT = decimal.Decimal(0)
+
+class MetaTaxImplication(core.PostingChecker):
+ ACCOUNTS = ('Assets:',)
+ METADATA_KEY = 'taxImplication'
+ VALUES_ENUM = core.MetadataEnum(METADATA_KEY, [
+ '1099',
+ 'Accountant-Advises-No-1099',
+ 'Bank-Transfer',
+ 'Foreign-Corporation',
+ 'Foreign-Individual-Contractor',
+ 'Fraud',
+ 'HSA-Contribution',
+ 'Loan',
+ 'Payroll',
+ 'Refund',
+ 'Reimbursement',
+ 'Retirement-Pretax',
+ 'Tax-Payment',
+ 'USA-501c3',
+ 'USA-Corporation',
+ 'USA-LLC-No-1099',
+ 'W2',
+ ], {})
+
+ def _should_check(self, txn, post):
+ return (
+ super()._should_check(txn, post)
+ and post.units.number < DEFAULT_STOP_AMOUNT
+ )
diff --git a/tests/test_meta_taxImplication.py b/tests/test_meta_taxImplication.py
new file mode 100644
index 0000000..1b1cf8a
--- /dev/null
+++ b/tests/test_meta_taxImplication.py
@@ -0,0 +1,130 @@
+"""Test handling of taxImplication metadata"""
+# Copyright © 2020 Brett Smith
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import pytest
+
+from . import testutil
+
+from conservancy_beancount.plugin import meta_tax_implication
+
+VALID_VALUES = {
+ '1099': '1099',
+ 'Accountant-Advises-No-1099': 'Accountant-Advises-No-1099',
+ 'Bank-Transfer': 'Bank-Transfer',
+ 'Foreign-Corporation': 'Foreign-Corporation',
+ 'Foreign-Individual-Contractor': 'Foreign-Individual-Contractor',
+ 'Fraud': 'Fraud',
+ 'HSA-Contribution': 'HSA-Contribution',
+ 'Loan': 'Loan',
+ 'Payroll': 'Payroll',
+ 'Refund': 'Refund',
+ 'Reimbursement': 'Reimbursement',
+ 'Retirement-Pretax': 'Retirement-Pretax',
+ 'Tax-Payment': 'Tax-Payment',
+ 'USA-501c3': 'USA-501c3',
+ 'USA-Corporation': 'USA-Corporation',
+ 'USA-LLC-No-1099': 'USA-LLC-No-1099',
+ 'W2': 'W2',
+}
+
+INVALID_VALUES = {
+ '199',
+ 'W3',
+ 'Payrol',
+ '',
+}
+
+@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
+def test_valid_values_on_postings(src_value, set_value):
+ txn = testutil.Transaction(postings=[
+ ('Accrued:AccountsPayable', 25),
+ ('Assets:Cash', -25, {'taxImplication': src_value}),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[-1])
+ assert not errors
+ assert txn.postings[-1].meta.get('taxImplication') == set_value
+
+@pytest.mark.parametrize('src_value', INVALID_VALUES)
+def test_invalid_values_on_postings(src_value):
+ txn = testutil.Transaction(postings=[
+ ('Accrued:AccountsPayable', 25),
+ ('Assets:Cash', -25, {'taxImplication': src_value}),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[-1])
+ assert errors
+
+@pytest.mark.parametrize('src_value,set_value', VALID_VALUES.items())
+def test_valid_values_on_transactions(src_value, set_value):
+ txn = testutil.Transaction(taxImplication=src_value, postings=[
+ ('Accrued:AccountsPayable', 25),
+ ('Assets:Cash', -25),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[-1])
+ assert not errors
+ assert txn.postings[-1].meta.get('taxImplication') == set_value
+
+@pytest.mark.parametrize('src_value', INVALID_VALUES)
+def test_invalid_values_on_transactions(src_value):
+ txn = testutil.Transaction(taxImplication=src_value, postings=[
+ ('Accrued:AccountsPayable', 25),
+ ('Assets:Cash', -25),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[-1])
+ assert errors
+
+@pytest.mark.parametrize('account', [
+ 'Accrued:AccountsPayable',
+ 'Expenses:General',
+ 'Liabilities:CreditCard',
+])
+def test_non_asset_accounts_skipped(account):
+ txn = testutil.Transaction(postings=[
+ (account, 25),
+ ('Assets:Cash', -25, {'taxImplication': 'USA-Corporation'}),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[0])
+ assert not errors
+
+def test_asset_credits_skipped():
+ txn = testutil.Transaction(postings=[
+ ('Income:Donations', -25),
+ ('Assets:Cash', 25),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[-1])
+ assert not errors
+ assert not txn.postings[-1].meta
+
+@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_default_value_set_in_date_range(date, need_value):
+ txn = testutil.Transaction(date=date, postings=[
+ ('Liabilites:CreditCard', 25),
+ ('Assets:Cash', -25),
+ ])
+ checker = meta_tax_implication.MetaTaxImplication()
+ errors = checker.check(txn, txn.postings[-1])
+ assert bool(errors) == bool(need_value)