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.
This commit is contained in:
		
							parent
							
								
									3519933b8c
								
							
						
					
					
						commit
						95fb8ce481
					
				
					 3 changed files with 62 additions and 15 deletions
				
			
		| 
						 | 
					@ -21,6 +21,9 @@ from ..beancount_types import (
 | 
				
			||||||
    Transaction,
 | 
					    Transaction,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FUND_KEY = 'project'
 | 
				
			||||||
 | 
					UNRESTRICTED_FUND = 'Conservancy'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MetaExpenseType(core._NormalizePostingMetadataHook):
 | 
					class MetaExpenseType(core._NormalizePostingMetadataHook):
 | 
				
			||||||
    VALUES_ENUM = core.MetadataEnum('expense-type', {
 | 
					    VALUES_ENUM = core.MetadataEnum('expense-type', {
 | 
				
			||||||
        'fundraising',
 | 
					        'fundraising',
 | 
				
			||||||
| 
						 | 
					@ -32,13 +35,35 @@ class MetaExpenseType(core._NormalizePostingMetadataHook):
 | 
				
			||||||
        'mgmt': 'management',
 | 
					        'mgmt': 'management',
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    DEFAULT_VALUES = {
 | 
					    DEFAULT_VALUES = {
 | 
				
			||||||
        'Expenses:Services:Accounting': VALUES_ENUM['management'],
 | 
					        'Expenses:Accounting': ('management', 'management'),
 | 
				
			||||||
        'Expenses:Services:Administration': VALUES_ENUM['management'],
 | 
					        'Expenses:BadDebt': ('management', 'program'),
 | 
				
			||||||
        'Expenses:Services:Fundraising': VALUES_ENUM['fundraising'],
 | 
					        '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:
 | 
					    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:
 | 
					    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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								setup.py
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
										
									
									
									
								
							| 
						 | 
					@ -5,7 +5,7 @@ from setuptools import setup
 | 
				
			||||||
setup(
 | 
					setup(
 | 
				
			||||||
    name='conservancy_beancount',
 | 
					    name='conservancy_beancount',
 | 
				
			||||||
    description="Plugin, library, and reports for reading Conservancy's books",
 | 
					    description="Plugin, library, and reports for reading Conservancy's books",
 | 
				
			||||||
    version='1.8.6',
 | 
					    version='1.8.7',
 | 
				
			||||||
    author='Software Freedom Conservancy',
 | 
					    author='Software Freedom Conservancy',
 | 
				
			||||||
    author_email='info@sfconservancy.org',
 | 
					    author_email='info@sfconservancy.org',
 | 
				
			||||||
    license='GNU AGPLv3+',
 | 
					    license='GNU AGPLv3+',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,9 @@ INVALID_VALUES = {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
TEST_KEY = 'expense-type'
 | 
					TEST_KEY = 'expense-type'
 | 
				
			||||||
 | 
					PROJECT_KEY = 'project'
 | 
				
			||||||
 | 
					UNRESTRICTED_FUND = 'Conservancy'
 | 
				
			||||||
 | 
					RESTRICTED_FUND = 'Alpha'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(scope='module')
 | 
					@pytest.fixture(scope='module')
 | 
				
			||||||
def hook():
 | 
					def hook():
 | 
				
			||||||
| 
						 | 
					@ -102,21 +105,40 @@ def test_non_expense_accounts_skipped(hook, account):
 | 
				
			||||||
    assert not errors
 | 
					    assert not errors
 | 
				
			||||||
    testutil.check_post_meta(txn, None, meta)
 | 
					    testutil.check_post_meta(txn, None, meta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize('account,set_value', [
 | 
					@pytest.mark.parametrize('account,exp_unrestricted,exp_restricted', [
 | 
				
			||||||
    ('Expenses:Services:Accounting', 'management'),
 | 
					    ('Expenses:Accounting', 'management', 'management'),
 | 
				
			||||||
    ('Expenses:Services:Administration', 'management'),
 | 
					    ('Expenses:BadDebt', 'management', 'program'),
 | 
				
			||||||
    ('Expenses:Services:Advocacy', 'program'),
 | 
					    ('Expenses:BankingFees', 'management', 'management'),
 | 
				
			||||||
    ('Expenses:Services:Development', 'program'),
 | 
					    ('Expenses:ComputerEquipment', 'management', 'program'),
 | 
				
			||||||
    ('Expenses:Services:Fundraising', 'fundraising'),
 | 
					    ('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=[
 | 
					    txn = testutil.Transaction(postings=[
 | 
				
			||||||
        ('Liabilites:CreditCard', -25),
 | 
					        ('Liabilites:CreditCard', -25),
 | 
				
			||||||
        (account, 25),
 | 
					        (account, 20, {PROJECT_KEY: UNRESTRICTED_FUND}),
 | 
				
			||||||
 | 
					        (account, 5, {PROJECT_KEY: RESTRICTED_FUND}),
 | 
				
			||||||
    ])
 | 
					    ])
 | 
				
			||||||
    errors = list(hook.run(txn))
 | 
					    errors = list(hook.run(txn))
 | 
				
			||||||
    assert not errors
 | 
					    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', [
 | 
					@pytest.mark.parametrize('date,set_value', [
 | 
				
			||||||
    (testutil.EXTREME_FUTURE_DATE, None),
 | 
					    (testutil.EXTREME_FUTURE_DATE, None),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue