data: Move iter_posting into Posting class methods.

As I move into reporting code, having Posting.from_beancount() is
handy, and then from_txn() might as well come along for the ride.
This commit is contained in:
Brett Smith 2020-04-11 16:16:35 -04:00
parent eb7f73e644
commit 14a87e792b
4 changed files with 52 additions and 28 deletions

View file

@ -264,6 +264,27 @@ class Posting(BasePosting):
# If it did, this declaration would pass without issue.
meta: Metadata # type:ignore[assignment]
@classmethod
def from_beancount(cls,
txn: Transaction,
index: int,
post: Optional[BasePosting]=None,
) -> 'Posting':
if post is None:
post = txn.postings[index]
return cls(
Account(post.account),
*post[1:5],
# see rationale above about Posting.meta
PostingMeta(txn, index, post), # type:ignore[arg-type]
)
@classmethod
def from_txn(cls, txn: Transaction) -> Iterable['Posting']:
"""Yield an enhanced Posting object for every posting in the transaction"""
for index, post in enumerate(txn.postings):
yield cls.from_beancount(txn, index, post)
def balance_of(txn: Transaction,
*preds: Callable[[Account], Optional[bool]],
@ -277,7 +298,7 @@ def balance_of(txn: Transaction,
balance_of uses the "weight" of each posting, so the return value will
use the currency of the postings' cost when available.
"""
match_posts = [post for post in iter_postings(txn)
match_posts = [post for post in Posting.from_txn(txn)
if any(pred(post.account) for pred in preds)]
number = decimal.Decimal(0)
if not match_posts:
@ -299,13 +320,3 @@ def is_opening_balance_txn(txn: Transaction) -> bool:
if not rest.currency:
return False
return abs(opening_equity.number + rest.number) < decimal.Decimal('.01')
def iter_postings(txn: Transaction) -> Iterator[Posting]:
"""Yield an enhanced Posting object for every posting in the transaction"""
for index, source in enumerate(txn.postings):
yield Posting(
Account(source.account),
*source[1:5],
# see rationale above about Posting.meta
PostingMeta(txn, index, source), # type:ignore[arg-type]
)

View file

@ -195,7 +195,7 @@ class _PostingHook(TransactionHook, metaclass=abc.ABCMeta):
def run(self, txn: Transaction) -> errormod.Iter:
if self._run_on_txn(txn):
for post in data.iter_postings(txn):
for post in data.Posting.from_txn(txn):
if self._run_on_post(txn, post):
yield from self.post_run(txn, post)

View file

@ -60,7 +60,7 @@ class MetaEntity(core.TransactionHook):
txn_entity_ok = False
if txn_entity_ok is False:
yield errormod.InvalidMetadataError(txn, self.METADATA_KEY, txn_entity)
for post in data.iter_postings(txn):
for post in data.Posting.from_txn(txn):
if not post.account.is_under(
'Assets:Receivable',
'Expenses',

View file

@ -1,4 +1,4 @@
"""Test data.iter_postings function"""
"""Test Posting methods"""
# Copyright © 2020 Brett Smith
#
# This program is free software: you can redistribute it and/or modify
@ -27,20 +27,33 @@ def simple_txn(index=None, key=None):
('Income:Donations', -5, {'note': 'donation love', 'extra': 'Extra'}),
])
def test_iter_postings(simple_txn):
for source, post in zip(simple_txn.postings, data.iter_postings(simple_txn)):
def test_from_beancount():
txn = testutil.Transaction(payee='Smith-Dakota', postings=[
('Income:Donations', -50),
('Assets:Cash', 50, {'receipt': 'cash-donation.pdf'}),
])
post = data.Posting.from_beancount(txn, 1)
# We don't just want to assert isinstance(post.attr, data.SomeClass);
# we also want to double-check that attributes were instantiated correctly.
assert post.account.is_under('Assets:Cash')
assert post.meta['receipt'] == 'cash-donation.pdf'
assert post.meta['entity'] == 'Smith-Dakota'
assert post.meta.date == testutil.FY_MID_DATE
def test_setting_metadata_propagates_to_source(simple_txn):
src_post = simple_txn.postings[1]
post = data.Posting.from_beancount(simple_txn, 1)
post.meta['edited'] = 'yes'
assert src_post.meta['edited'] == 'yes'
assert not isinstance(src_post.meta, data.PostingMeta)
def test_deleting_metadata_propagates_to_source(simple_txn):
post = data.Posting.from_beancount(simple_txn, 1)
del post.meta['extra']
assert 'extra' not in simple_txn.postings[1].meta
def test_from_txn(simple_txn):
for source, post in zip(simple_txn.postings, data.Posting.from_txn(simple_txn)):
assert all(source[x] == post[x] for x in range(len(source) - 1))
assert isinstance(post.account, data.Account)
assert post.meta['note'] # Only works with PostingMeta
def test_setting_metadata_propagates_to_source(simple_txn):
for index, post in enumerate(data.iter_postings(simple_txn)):
post.meta['edited'] = str(index)
for index, post in enumerate(simple_txn.postings):
assert post.meta['edited'] == str(index)
assert not isinstance(post.meta, data.PostingMeta)
def test_deleting_metadata_propagates_to_source(simple_txn):
posts = list(data.iter_postings(simple_txn))
del posts[1].meta['extra']
assert 'extra' not in simple_txn.postings[1].meta