diff --git a/conservancy_beancount/plugin/meta_project.py b/conservancy_beancount/plugin/meta_project.py index 0014706ba196258db5638bad53b768e3464726f3..ef653d1d3992dcb6995e3f320917cff08765a401 100644 --- a/conservancy_beancount/plugin/meta_project.py +++ b/conservancy_beancount/plugin/meta_project.py @@ -80,7 +80,7 @@ class MetaProject(core._NormalizePostingMetadataHook): ) def _run_on_opening_post(self, txn: Transaction, post: data.Posting) -> bool: - return post.account.is_under(self.RESTRICTED_FUNDS_ACCT) is not None + return post.account.is_under('Equity') is not None def _run_on_other_post(self, txn: Transaction, post: data.Posting) -> bool: if post.account.is_under('Liabilities'): @@ -88,6 +88,7 @@ class MetaProject(core._NormalizePostingMetadataHook): else: return post.account.is_under( 'Assets:Receivable', + 'Equity', 'Expenses', 'Income', self.RESTRICTED_FUNDS_ACCT, @@ -105,6 +106,25 @@ class MetaProject(core._NormalizePostingMetadataHook): def _run_on_txn(self, txn: Transaction) -> bool: return txn.date in self.TXN_DATE_RANGE + def post_run(self, txn: Transaction, post: data.Posting) -> errormod.Iter: + if (post.account.is_under('Equity') + and not post.account.is_under(self.RESTRICTED_FUNDS_ACCT)): + # Force all unrestricted Equity accounts to have the default + # project. This is what our fiscal controls policy says, and + # setting it here simplifies higher-level queries and reporting. + post_value = post.meta.get(self.METADATA_KEY) + txn_value = txn.meta.get(self.METADATA_KEY) + # Only report an error if the posting specifically had a different + # value, not if it just inherited it from the transaction. + if (post_value is not txn_value + and post_value != self.DEFAULT_PROJECT): + yield errormod.InvalidMetadataError( + txn, self.METADATA_KEY, post_value, post, + ) + post.meta[self.METADATA_KEY] = self.DEFAULT_PROJECT + else: + yield from super().post_run(txn, post) + def run(self, txn: Transaction) -> errormod.Iter: # mypy says we can't assign over a method. # I understand why it wants to enforce thas as a blanket rule, but diff --git a/setup.py b/setup.py index 594df7e545f1a2529bb5c805c90c9aed25553561..385fa5bbbef687b5e9ed0d78b408fc27d445ecbf 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.2.4', + version='1.2.5', author='Software Freedom Conservancy', author_email='info@sfconservancy.org', license='GNU AGPLv3+', diff --git a/tests/test_meta_project.py b/tests/test_meta_project.py index 6178f6827d935595258b4345ca9cddb6f4a723b4..cee3dc17a57ab3a731731d63821205b139657a8d 100644 --- a/tests/test_meta_project.py +++ b/tests/test_meta_project.py @@ -109,6 +109,8 @@ def test_which_accounts_required_on(hook, account, required): assert required == any(errors) @pytest.mark.parametrize('account', [ + 'Equity:Funds:Unrestricted', + 'Equity:Realized:CurrencyConversion', 'Expenses:Payroll:Salary', 'Expenses:Payroll:Tax', 'Liabilities:Payable:Vacation', @@ -122,6 +124,38 @@ def test_default_values(hook, account): assert not errors testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE}) +@pytest.mark.parametrize('equity,other_acct,value', testutil.combine_values( + ['Equity:Funds:Unrestricted', 'Equity:Realized:CurrencyConversion'], + ['Assets:Checking', 'Liabilities:CreditCard'], + VALID_VALUES, +)) +def test_equity_override_txn_meta(hook, equity, other_acct, value): + if value == DEFAULT_VALUE: + value = f'Not{value}' + txn = testutil.Transaction(**{TEST_KEY: value}, postings=[ + (other_acct, 100), + (equity, -100), + ]) + errors = list(hook.run(txn)) + assert not errors + testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE}) + +@pytest.mark.parametrize('equity,other_acct,value', testutil.combine_values( + ['Equity:Funds:Unrestricted', 'Equity:Realized:CurrencyConversion'], + ['Assets:Checking', 'Liabilities:CreditCard'], + VALID_VALUES, +)) +def test_equity_override_post_meta(hook, equity, other_acct, value): + if value == DEFAULT_VALUE: + value = f'Not{value}' + txn = testutil.Transaction(postings=[ + (other_acct, 100), + (equity, -100, {TEST_KEY: value}), + ]) + actual = {error.message for error in hook.run(txn)} + assert actual == {f"{equity} has invalid {TEST_KEY}: {value}"} + testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE}) + @pytest.mark.parametrize('date,required', [ (testutil.EXTREME_FUTURE_DATE, False), (testutil.FUTURE_DATE, True),