plugin: Most validations skip opening balance transactions. RT#10642.
This commit is contained in:
parent
4eaba1ebf6
commit
9f0c30738d
10 changed files with 78 additions and 4 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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'}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue