accrual: Check the consistency of accruals' cost.

This commit is contained in:
Brett Smith 2020-05-30 10:35:29 -04:00
parent a008a09477
commit aef00ce83f
3 changed files with 34 additions and 10 deletions

View file

@ -67,6 +67,7 @@ import re
import sys import sys
from typing import ( from typing import (
Any,
Callable, Callable,
Dict, Dict,
Iterable, Iterable,
@ -328,19 +329,28 @@ class SearchTerm(NamedTuple):
) )
return cls(key, pattern) return cls(key, pattern)
def _consistency_check_one_thing(
key: MetaValue,
related: core.RelatedPostings,
get_name: str,
get_func: Callable[[data.Posting], Any],
) -> Iterable[Error]:
values = {get_func(post) for post in related}
if len(values) != 1:
for post in related:
errmsg = f'inconsistent {get_name} for invoice {key}: {get_func(post)}'
yield Error(post.meta, errmsg, post.meta.txn)
def consistency_check(groups: PostGroups) -> Iterable[Error]: def consistency_check(groups: PostGroups) -> Iterable[Error]:
errfmt = 'inconsistent {} for invoice {}: {{}}'
for key, related in groups.items(): for key, related in groups.items():
yield from _consistency_check_one_thing(
key, related, 'cost', lambda post: post.cost,
)
for checked_meta in ['contract', 'entity', 'purchase-order']: for checked_meta in ['contract', 'entity', 'purchase-order']:
meta_values = related.meta_values(checked_meta) yield from _consistency_check_one_thing(
if len(meta_values) != 1: key, related, checked_meta, lambda post: post.meta.get(checked_meta),
errmsg = f'inconsistent {checked_meta} for invoice {key}' )
for post in related:
yield Error(
post.meta,
f'{errmsg}: {post.meta.get(checked_meta)}',
post.meta.txn,
)
def filter_search(postings: Iterable[data.Posting], def filter_search(postings: Iterable[data.Posting],
search_terms: Iterable[SearchTerm], search_terms: Iterable[SearchTerm],

View file

@ -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.0.8', version='1.0.9',
author='Software Freedom Conservancy', author='Software Freedom Conservancy',
author_email='info@sfconservancy.org', author_email='info@sfconservancy.org',
license='GNU AGPLv3+', license='GNU AGPLv3+',

View file

@ -250,6 +250,20 @@ def test_consistency_check_when_inconsistent(meta_key, account):
assert actual.entry is txn assert actual.entry is txn
assert actual.source.get('lineno') == exp_lineno assert actual.source.get('lineno') == exp_lineno
def test_consistency_check_cost():
account = ACCOUNTS[0]
invoice = 'test-cost-invoice'
txn = testutil.Transaction(postings=[
(account, 100, 'EUR', ('1.1251', 'USD'), {'invoice': invoice, 'lineno': 1}),
(account, -100, 'EUR', ('1.125', 'USD'), {'invoice': invoice, 'lineno': 2}),
])
related = core.RelatedPostings(data.Posting.from_txn(txn))
errors = list(accrual.consistency_check({invoice: related}))
for post, err in itertools.zip_longest(txn.postings, errors):
assert err.message == f'inconsistent cost for invoice {invoice}: {post.cost}'
assert err.entry is txn
assert err.source.get('lineno') == post.meta['lineno']
def check_output(output, expect_patterns): def check_output(output, expect_patterns):
output.seek(0) output.seek(0)
testutil.check_lines_match(iter(output), expect_patterns) testutil.check_lines_match(iter(output), expect_patterns)