meta_project: Force the default project on Equity accounts.
See rationale in comments.
This commit is contained in:
parent
8b8bdc0225
commit
d7e2ab34b9
3 changed files with 56 additions and 2 deletions
|
@ -80,7 +80,7 @@ class MetaProject(core._NormalizePostingMetadataHook):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _run_on_opening_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
|
return post.account.is_under('Equity') is not None
|
||||||
|
|
||||||
def _run_on_other_post(self, txn: Transaction, post: data.Posting) -> bool:
|
def _run_on_other_post(self, txn: Transaction, post: data.Posting) -> bool:
|
||||||
if post.account.is_under('Liabilities'):
|
if post.account.is_under('Liabilities'):
|
||||||
|
@ -88,6 +88,7 @@ class MetaProject(core._NormalizePostingMetadataHook):
|
||||||
else:
|
else:
|
||||||
return post.account.is_under(
|
return post.account.is_under(
|
||||||
'Assets:Receivable',
|
'Assets:Receivable',
|
||||||
|
'Equity',
|
||||||
'Expenses',
|
'Expenses',
|
||||||
'Income',
|
'Income',
|
||||||
self.RESTRICTED_FUNDS_ACCT,
|
self.RESTRICTED_FUNDS_ACCT,
|
||||||
|
@ -105,6 +106,25 @@ class MetaProject(core._NormalizePostingMetadataHook):
|
||||||
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
|
||||||
|
|
||||||
|
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:
|
def run(self, txn: Transaction) -> errormod.Iter:
|
||||||
# mypy says we can't assign over a method.
|
# mypy says we can't assign over a method.
|
||||||
# I understand why it wants to enforce thas as a blanket rule, but
|
# I understand why it wants to enforce thas as a blanket rule, but
|
||||||
|
|
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.2.4',
|
version='1.2.5',
|
||||||
author='Software Freedom Conservancy',
|
author='Software Freedom Conservancy',
|
||||||
author_email='info@sfconservancy.org',
|
author_email='info@sfconservancy.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
|
|
@ -109,6 +109,8 @@ def test_which_accounts_required_on(hook, account, required):
|
||||||
assert required == any(errors)
|
assert required == any(errors)
|
||||||
|
|
||||||
@pytest.mark.parametrize('account', [
|
@pytest.mark.parametrize('account', [
|
||||||
|
'Equity:Funds:Unrestricted',
|
||||||
|
'Equity:Realized:CurrencyConversion',
|
||||||
'Expenses:Payroll:Salary',
|
'Expenses:Payroll:Salary',
|
||||||
'Expenses:Payroll:Tax',
|
'Expenses:Payroll:Tax',
|
||||||
'Liabilities:Payable:Vacation',
|
'Liabilities:Payable:Vacation',
|
||||||
|
@ -122,6 +124,38 @@ def test_default_values(hook, account):
|
||||||
assert not errors
|
assert not errors
|
||||||
testutil.check_post_meta(txn, None, {TEST_KEY: DEFAULT_VALUE})
|
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', [
|
@pytest.mark.parametrize('date,required', [
|
||||||
(testutil.EXTREME_FUTURE_DATE, False),
|
(testutil.EXTREME_FUTURE_DATE, False),
|
||||||
(testutil.FUTURE_DATE, True),
|
(testutil.FUTURE_DATE, True),
|
||||||
|
|
Loading…
Reference in a new issue