From 95fb8ce4815ee49f583dabb0f926839927659e64 2020-08-20 21:22:05 From: Brett Smith Date: 2020-08-20 21:22:05 Subject: [PATCH] meta_expense_type: Refine defaults. * Default "management" for more accounts. * There's a good handful of accounts where in past audits, the functional split has been "Conservancy expenses are management, project expenses are program." Handle those cases too. --- diff --git a/conservancy_beancount/plugin/meta_expense_type.py b/conservancy_beancount/plugin/meta_expense_type.py index ce03a1a16d6be5bcd9d77eee93568aca07af63e5..c42314b3661d85e1e0e740cb4a59560384502dd6 100644 --- a/conservancy_beancount/plugin/meta_expense_type.py +++ b/conservancy_beancount/plugin/meta_expense_type.py @@ -21,6 +21,9 @@ from ..beancount_types import ( Transaction, ) +FUND_KEY = 'project' +UNRESTRICTED_FUND = 'Conservancy' + class MetaExpenseType(core._NormalizePostingMetadataHook): VALUES_ENUM = core.MetadataEnum('expense-type', { 'fundraising', @@ -32,13 +35,35 @@ class MetaExpenseType(core._NormalizePostingMetadataHook): 'mgmt': 'management', }) DEFAULT_VALUES = { - 'Expenses:Services:Accounting': VALUES_ENUM['management'], - 'Expenses:Services:Administration': VALUES_ENUM['management'], - 'Expenses:Services:Fundraising': VALUES_ENUM['fundraising'], + 'Expenses:Accounting': ('management', 'management'), + 'Expenses:BadDebt': ('management', 'program'), + 'Expenses:BankingFees': ('management', 'management'), + 'Expenses:ComputerEquipment': ('management', 'program'), + 'Expenses:Fines': ('management', 'program'), + 'Expenses:FilingFees': ('management', 'program'), + 'Expenses:Hosting': ('management', 'program'), + 'Expenses:Insurance': ('management', 'management'), + 'Expenses:Office': ('management', 'program'), + 'Expenses:Other': ('management', 'program'), + 'Expenses:Phones': ('management', 'program'), + 'Expenses:Postage': ('management', 'program'), + 'Expenses:ProfessionalMemberships': ('management', 'program'), + 'Expenses:Services:Accounting': ('management', 'management'), + 'Expenses:Services:Administration': ('management', 'management'), + 'Expenses:Services:Fundraising': ('fundraising', 'fundraising'), + 'Expenses:Travel': ('management', 'management'), } def _run_on_post(self, txn: Transaction, post: data.Posting) -> bool: - return post.account.startswith('Expenses:') + return post.account.is_under('Expenses') is not None def _default_value(self, txn: Transaction, post: data.Posting) -> MetaValueEnum: - return self.DEFAULT_VALUES.get(post.account, 'program') + key = post.account.is_under(*self.DEFAULT_VALUES) + if key is None: + return 'program' + else: + unrestricted, restricted = self.DEFAULT_VALUES[key] + if post.meta.get(FUND_KEY) == UNRESTRICTED_FUND: + return unrestricted + else: + return restricted diff --git a/setup.py b/setup.py index bab6ee30e78f06d6b5007efb7324fee444907fd4..e6c61ead44bb7b922288c5d3db0a00e396409a97 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='conservancy_beancount', description="Plugin, library, and reports for reading Conservancy's books", - version='1.8.6', + version='1.8.7', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+', diff --git a/tests/test_meta_expense_type.py b/tests/test_meta_expense_type.py index c8289a8288a48bd55031c398935b89106b009fa1..ab57f4b622ec60eb5c37fa9c5cc790ae923b9040 100644 --- a/tests/test_meta_expense_type.py +++ b/tests/test_meta_expense_type.py @@ -38,6 +38,9 @@ INVALID_VALUES = { } TEST_KEY = 'expense-type' +PROJECT_KEY = 'project' +UNRESTRICTED_FUND = 'Conservancy' +RESTRICTED_FUND = 'Alpha' @pytest.fixture(scope='module') def hook(): @@ -102,21 +105,40 @@ def test_non_expense_accounts_skipped(hook, account): assert not errors testutil.check_post_meta(txn, None, meta) -@pytest.mark.parametrize('account,set_value', [ - ('Expenses:Services:Accounting', 'management'), - ('Expenses:Services:Administration', 'management'), - ('Expenses:Services:Advocacy', 'program'), - ('Expenses:Services:Development', 'program'), - ('Expenses:Services:Fundraising', 'fundraising'), +@pytest.mark.parametrize('account,exp_unrestricted,exp_restricted', [ + ('Expenses:Accounting', 'management', 'management'), + ('Expenses:BadDebt', 'management', 'program'), + ('Expenses:BankingFees', 'management', 'management'), + ('Expenses:ComputerEquipment', 'management', 'program'), + ('Expenses:Fines', 'management', 'program'), + ('Expenses:FilingFees', 'management', 'program'), + ('Expenses:Hosting', 'management', 'program'), + ('Expenses:Insurance', 'management', 'management'), + ('Expenses:Office', 'management', 'program'), + ('Expenses:Other', 'management', 'program'), + ('Expenses:Phones', 'management', 'program'), + ('Expenses:Postage', 'management', 'program'), + ('Expenses:ProfessionalMemberships', 'management', 'program'), + ('Expenses:Services:Accounting', 'management', 'management'), + ('Expenses:Services:Administration', 'management', 'management'), + ('Expenses:Services:Advocacy', 'program', 'program'), + ('Expenses:Services:Development', 'program', 'program'), + ('Expenses:Services:Fundraising', 'fundraising', 'fundraising'), + ('Expenses:Travel', 'management', 'management'), ]) -def test_default_values(hook, account, set_value): +def test_default_values(hook, account, exp_unrestricted, exp_restricted): txn = testutil.Transaction(postings=[ ('Liabilites:CreditCard', -25), - (account, 25), + (account, 20, {PROJECT_KEY: UNRESTRICTED_FUND}), + (account, 5, {PROJECT_KEY: RESTRICTED_FUND}), ]) errors = list(hook.run(txn)) assert not errors - testutil.check_post_meta(txn, None, {TEST_KEY: set_value}) + testutil.check_post_meta( + txn, None, + {TEST_KEY: exp_unrestricted, PROJECT_KEY: UNRESTRICTED_FUND}, + {TEST_KEY: exp_restricted, PROJECT_KEY: RESTRICTED_FUND}, + ) @pytest.mark.parametrize('date,set_value', [ (testutil.EXTREME_FUTURE_DATE, None),