meta_project: Force the default project on Equity accounts.

See rationale in comments.
This commit is contained in:
Brett Smith 2020-06-17 04:28:43 -04:00
parent 8b8bdc0225
commit d7e2ab34b9
3 changed files with 56 additions and 2 deletions

View file

@ -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

View file

@ -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+',

View file

@ -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),