plugin: Most validations skip opening balance transactions. RT#10642.

This commit is contained in:
Brett Smith 2020-04-09 15:12:04 -04:00
parent 4eaba1ebf6
commit 9f0c30738d
10 changed files with 78 additions and 4 deletions

View file

@ -185,7 +185,10 @@ class _PostingHook(TransactionHook, metaclass=abc.ABCMeta):
cls.HOOK_GROUPS = cls.HOOK_GROUPS.union(['posting']) cls.HOOK_GROUPS = cls.HOOK_GROUPS.union(['posting'])
def _run_on_txn(self, txn: Transaction) -> bool: 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: def _run_on_post(self, txn: Transaction, post: data.Posting) -> bool:
return True return True

View file

@ -49,6 +49,8 @@ class MetaEntity(core.TransactionHook):
del alnum del alnum
def run(self, txn: Transaction) -> errormod.Iter: 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) txn_entity = txn.meta.get(self.METADATA_KEY, txn.payee)
if txn_entity is None: if txn_entity is None:
txn_entity_ok = None txn_entity_ok = None

View file

@ -40,6 +40,7 @@ class MetaProject(core._NormalizePostingMetadataHook):
DEFAULT_PROJECT = 'Conservancy' DEFAULT_PROJECT = 'Conservancy'
PROJECT_DATA_PATH = Path('Projects', 'project-data.yml') PROJECT_DATA_PATH = Path('Projects', 'project-data.yml')
VALUES_ENUM = core.MetadataEnum('project', {DEFAULT_PROJECT}) 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: def __init__(self, config: configmod.Config, source_path: Path=PROJECT_DATA_PATH) -> None:
repo_path = config.repository_path() repo_path = config.repository_path()
@ -78,7 +79,10 @@ class MetaProject(core._NormalizePostingMetadataHook):
source=source, 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'): if post.account.is_under('Liabilities'):
return not post.account.is_credit_card() return not post.account.is_credit_card()
else: else:
@ -86,6 +90,7 @@ class MetaProject(core._NormalizePostingMetadataHook):
'Assets:Receivable', 'Assets:Receivable',
'Expenses', 'Expenses',
'Income', 'Income',
self.RESTRICTED_FUNDS_ACCT,
) is not None ) is not None
def _default_value(self, txn: Transaction, post: data.Posting) -> MetaValueEnum: def _default_value(self, txn: Transaction, post: data.Posting) -> MetaValueEnum:
@ -96,3 +101,17 @@ class MetaProject(core._NormalizePostingMetadataHook):
return self.DEFAULT_PROJECT return self.DEFAULT_PROJECT
else: else:
raise errormod.InvalidMetadataError(txn, self.METADATA_KEY, None, post) 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)

View file

@ -165,3 +165,7 @@ def test_which_accounts_required_on(hook, account, required):
assert errors assert errors
assert any(error.message == "{} missing entity".format(account) assert any(error.message == "{} missing entity".format(account)
for error in errors) for error in errors)
def test_not_required_on_opening(hook):
txn = testutil.Transaction.opening_balance()
assert not list(hook.run(txn))

View file

@ -29,7 +29,7 @@ REQUIRED_ACCOUNTS = {
NON_REQUIRED_ACCOUNTS = { NON_REQUIRED_ACCOUNTS = {
'Assets:Cash', 'Assets:Cash',
'Equity:OpeningBalance', 'Equity:Retained',
'Expenses:Other', 'Expenses:Other',
'Income:Other', 'Income:Other',
'Liabilities:CreditCard', 'Liabilities:CreditCard',
@ -140,3 +140,7 @@ def test_missing_invoice(hook, acct1, acct2):
]) ])
actual = {error.message for error in hook.run(txn)} actual = {error.message for error in hook.run(txn)}
assert actual == {"{} missing {}".format(acct1, TEST_KEY)} 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))

View file

@ -163,3 +163,11 @@ def test_paid_accts_not_checked(hook):
def test_does_not_apply_to_other_accounts(hook, account): def test_does_not_apply_to_other_accounts(hook, account):
meta = seed_meta() meta = seed_meta()
check(hook, None, account, post_meta=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))

View file

@ -187,3 +187,10 @@ def test_invoice_payment_transaction_ok(hook, txn_id, inv_id):
('Expenses:BankingFees', 3), ('Expenses:BankingFees', 3),
]) ])
assert not list(hook.run(txn)) 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))

View file

@ -90,6 +90,8 @@ def test_invalid_values_on_transactions(hook, src_value):
('Assets:Receivable:Accounts', True), ('Assets:Receivable:Accounts', True),
('Assets:Receivable:Loans', True), ('Assets:Receivable:Loans', True),
('Equity:OpeningBalance', False), ('Equity:OpeningBalance', False),
('Equity:Funds:Restricted', True),
('Equity:Funds:Unrestricted', False),
('Expenses:General', True), ('Expenses:General', True),
('Income:Donations', True), ('Income:Donations', True),
('Liabilities:CreditCard', False), ('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) config = testutil.TestConfig(repo_path=repo_path_s)
with pytest.raises(errormod.ConfigurationError): with pytest.raises(errormod.ConfigurationError):
meta_project.MetaProject(config, Path(data_path_s)) 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'}

View file

@ -86,7 +86,7 @@ NOT_REQUIRED_ACCOUNTS = itertools.cycle([
'Assets:PayPal', 'Assets:PayPal',
'Assets:Prepaid:Expenses', 'Assets:Prepaid:Expenses',
'Assets:Receivable:Accounts', 'Assets:Receivable:Accounts',
'Equity:OpeningBalance', 'Equity:Retained',
'Expenses:Other', 'Expenses:Other',
'Income:Other', 'Income:Other',
'Liabilities:Payable:Accounts', '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}), (test_acct.name, 0, {test_acct.fallback_meta: value}),
]) ])
assert not list(hook.run(txn)) 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)

View file

@ -205,3 +205,11 @@ def test_configuration_error_without_rt():
config = testutil.TestConfig() config = testutil.TestConfig()
with pytest.raises(errormod.ConfigurationError): with pytest.raises(errormod.ConfigurationError):
meta_receivable_documentation.MetaReceivableDocumentation(config) 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))