diff --git a/conservancy_beancount/plugin/core.py b/conservancy_beancount/plugin/core.py index 550b5a2c4b8e9a5566ac6372361eb95de3f975bf..a5667e726d9d92964bfdc10d1cfbbab49cddaa72 100644 --- a/conservancy_beancount/plugin/core.py +++ b/conservancy_beancount/plugin/core.py @@ -185,7 +185,10 @@ class _PostingHook(TransactionHook, metaclass=abc.ABCMeta): cls.HOOK_GROUPS = cls.HOOK_GROUPS.union(['posting']) def _run_on_txn(self, txn: Transaction) -> bool: - return txn.date in self.TXN_DATE_RANGE + return ( + txn.date in self.TXN_DATE_RANGE + and not data.is_opening_balance_txn(txn) + ) def _run_on_post(self, txn: Transaction, post: data.Posting) -> bool: return True diff --git a/conservancy_beancount/plugin/meta_entity.py b/conservancy_beancount/plugin/meta_entity.py index 7f4ecf1ffad82d85c51ffe46d3d4a8e0309975f8..d39997d353e8afdfc41f78be6ce65a1cb8c225ce 100644 --- a/conservancy_beancount/plugin/meta_entity.py +++ b/conservancy_beancount/plugin/meta_entity.py @@ -49,6 +49,8 @@ class MetaEntity(core.TransactionHook): del alnum def run(self, txn: Transaction) -> errormod.Iter: + if data.is_opening_balance_txn(txn): + return txn_entity = txn.meta.get(self.METADATA_KEY, txn.payee) if txn_entity is None: txn_entity_ok = None diff --git a/conservancy_beancount/plugin/meta_project.py b/conservancy_beancount/plugin/meta_project.py index b47d9fe6ab20aaf16afb90bedc7ccdab513cf90f..0014706ba196258db5638bad53b768e3464726f3 100644 --- a/conservancy_beancount/plugin/meta_project.py +++ b/conservancy_beancount/plugin/meta_project.py @@ -40,6 +40,7 @@ class MetaProject(core._NormalizePostingMetadataHook): DEFAULT_PROJECT = 'Conservancy' PROJECT_DATA_PATH = Path('Projects', 'project-data.yml') VALUES_ENUM = core.MetadataEnum('project', {DEFAULT_PROJECT}) + RESTRICTED_FUNDS_ACCT = 'Equity:Funds:Restricted' def __init__(self, config: configmod.Config, source_path: Path=PROJECT_DATA_PATH) -> None: repo_path = config.repository_path() @@ -78,7 +79,10 @@ class MetaProject(core._NormalizePostingMetadataHook): source=source, ) - def _run_on_post(self, txn: Transaction, post: data.Posting) -> bool: + def _run_on_opening_post(self, txn: Transaction, post: data.Posting) -> bool: + return post.account.is_under(self.RESTRICTED_FUNDS_ACCT) is not None + + def _run_on_other_post(self, txn: Transaction, post: data.Posting) -> bool: if post.account.is_under('Liabilities'): return not post.account.is_credit_card() else: @@ -86,6 +90,7 @@ class MetaProject(core._NormalizePostingMetadataHook): 'Assets:Receivable', 'Expenses', 'Income', + self.RESTRICTED_FUNDS_ACCT, ) is not None def _default_value(self, txn: Transaction, post: data.Posting) -> MetaValueEnum: @@ -96,3 +101,17 @@ class MetaProject(core._NormalizePostingMetadataHook): return self.DEFAULT_PROJECT else: raise errormod.InvalidMetadataError(txn, self.METADATA_KEY, None, post) + + def _run_on_txn(self, txn: Transaction) -> bool: + return txn.date in self.TXN_DATE_RANGE + + 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 + # we're substituting in another type-compatible method, so it's pretty + # safe. + if data.is_opening_balance_txn(txn): + self._run_on_post = self._run_on_opening_post # type:ignore[assignment] + else: + self._run_on_post = self._run_on_other_post # type:ignore[assignment] + return super().run(txn) diff --git a/tests/test_meta_entity.py b/tests/test_meta_entity.py index f9caac672f38310b313b0a601b489658a7dbc7a2..58646e4cdbd3f729eddf13221f95328f9193124c 100644 --- a/tests/test_meta_entity.py +++ b/tests/test_meta_entity.py @@ -165,3 +165,7 @@ def test_which_accounts_required_on(hook, account, required): assert errors assert any(error.message == "{} missing entity".format(account) for error in errors) + +def test_not_required_on_opening(hook): + txn = testutil.Transaction.opening_balance() + assert not list(hook.run(txn)) diff --git a/tests/test_meta_invoice.py b/tests/test_meta_invoice.py index 7de49c5016799d6a08c4aa3887d0ba290ebd9d06..4cd70e1665c2459284f20712eb371cdb934928d6 100644 --- a/tests/test_meta_invoice.py +++ b/tests/test_meta_invoice.py @@ -29,7 +29,7 @@ REQUIRED_ACCOUNTS = { NON_REQUIRED_ACCOUNTS = { 'Assets:Cash', - 'Equity:OpeningBalance', + 'Equity:Retained', 'Expenses:Other', 'Income:Other', 'Liabilities:CreditCard', @@ -140,3 +140,7 @@ def test_missing_invoice(hook, acct1, acct2): ]) actual = {error.message for error in hook.run(txn)} assert actual == {"{} missing {}".format(acct1, TEST_KEY)} + +def test_not_required_on_opening(hook): + txn = testutil.Transaction.opening_balance() + assert not list(hook.run(txn)) diff --git a/tests/test_meta_payable_documentation.py b/tests/test_meta_payable_documentation.py index 4b6a4faa284b073113eba5084a85a703f286ca6c..7cabd8ecc5f2f84e528cfaa4f80c77b64e72d4d9 100644 --- a/tests/test_meta_payable_documentation.py +++ b/tests/test_meta_payable_documentation.py @@ -163,3 +163,11 @@ def test_paid_accts_not_checked(hook): def test_does_not_apply_to_other_accounts(hook, account): meta = seed_meta() check(hook, None, account, post_meta=meta) + +def test_not_required_on_opening(hook): + txn = testutil.Transaction(postings=[ + ('Liabilities:Payable:Accounts', -15), + ('Liabilities:Payable:Vacation', -25), + (next(testutil.OPENING_EQUITY_ACCOUNTS), 40), + ]) + assert not list(hook.run(txn)) diff --git a/tests/test_meta_paypal_id.py b/tests/test_meta_paypal_id.py index c70d99ae50528a589a64ef9d0057209b0c7a0a17..49d736ee0f47521f98afd521f3a612760187e618 100644 --- a/tests/test_meta_paypal_id.py +++ b/tests/test_meta_paypal_id.py @@ -187,3 +187,10 @@ def test_invoice_payment_transaction_ok(hook, txn_id, inv_id): ('Expenses:BankingFees', 3), ]) assert not list(hook.run(txn)) + +def test_not_required_on_opening(hook): + txn = testutil.Transaction(postings=[ + ('Assets:PayPal', 1000), + (next(testutil.OPENING_EQUITY_ACCOUNTS), -1000), + ]) + assert not list(hook.run(txn)) diff --git a/tests/test_meta_project.py b/tests/test_meta_project.py index 5c372dcccf5756ebac80640d709f158bccbdfc54..5149e356d4634bb31118c0e14ad93861cc9e6334 100644 --- a/tests/test_meta_project.py +++ b/tests/test_meta_project.py @@ -90,6 +90,8 @@ def test_invalid_values_on_transactions(hook, src_value): ('Assets:Receivable:Accounts', True), ('Assets:Receivable:Loans', True), ('Equity:OpeningBalance', False), + ('Equity:Funds:Restricted', True), + ('Equity:Funds:Unrestricted', False), ('Expenses:General', True), ('Income:Donations', True), ('Liabilities:CreditCard', False), @@ -154,3 +156,13 @@ def test_invalid_project_data(repo_path_s, data_path_s): config = testutil.TestConfig(repo_path=repo_path_s) with pytest.raises(errormod.ConfigurationError): meta_project.MetaProject(config, Path(data_path_s)) + +def test_not_required_on_opening(hook): + txn = testutil.Transaction.opening_balance('Equity:Funds:Unrestricted') + assert not list(hook.run(txn)) + +def test_always_required_on_restricted_funds(hook): + acct = 'Equity:Funds:Restricted' + txn = testutil.Transaction.opening_balance(acct) + actual = {error.message for error in hook.run(txn)} + assert actual == {f'{acct} missing project'} diff --git a/tests/test_meta_receipt.py b/tests/test_meta_receipt.py index b251ddafaf1b0d9eb14cd8eac7fbe19b5f37953b..af7a37025cec0929239a0d76eff0dbfa99fbfa1c 100644 --- a/tests/test_meta_receipt.py +++ b/tests/test_meta_receipt.py @@ -86,7 +86,7 @@ NOT_REQUIRED_ACCOUNTS = itertools.cycle([ 'Assets:PayPal', 'Assets:Prepaid:Expenses', 'Assets:Receivable:Accounts', - 'Equity:OpeningBalance', + 'Equity:Retained', 'Expenses:Other', 'Income:Other', 'Liabilities:Payable:Accounts', @@ -342,3 +342,10 @@ def test_fallback_on_zero_amount_postings(hook, test_acct, other_acct, value): (test_acct.name, 0, {test_acct.fallback_meta: value}), ]) assert not list(hook.run(txn)) + +@pytest.mark.parametrize('test_acct,equity_acct', testutil.combine_values( + ACCOUNTS, + testutil.OPENING_EQUITY_ACCOUNTS, +)) +def test_not_required_on_opening(hook, test_acct, equity_acct): + check(hook, test_acct, equity_acct, None) diff --git a/tests/test_meta_receivable_documentation.py b/tests/test_meta_receivable_documentation.py index 0843e2ce22d3c498ae920ad4476d67160015ae30..ad41abb6a1b35a7b7fd9e17d9210fb9bde8e4ad3 100644 --- a/tests/test_meta_receivable_documentation.py +++ b/tests/test_meta_receivable_documentation.py @@ -205,3 +205,11 @@ def test_configuration_error_without_rt(): config = testutil.TestConfig() with pytest.raises(errormod.ConfigurationError): meta_receivable_documentation.MetaReceivableDocumentation(config) + +def test_not_required_on_opening(hook): + txn = testutil.Transaction(postings=[ + ('Assets:Receivable:Accounts', 100), + ('Assets:Receivable:Loans', 200), + (next(testutil.OPENING_EQUITY_ACCOUNTS), -300), + ]) + assert not list(hook.run(txn))