diff --git a/conservancy_beancount/reports/core.py b/conservancy_beancount/reports/core.py index cf38125..5ded97f 100644 --- a/conservancy_beancount/reports/core.py +++ b/conservancy_beancount/reports/core.py @@ -199,6 +199,16 @@ class RelatedPostings(Sequence[data.Posting]): except NameError: return Balance() + def balance_at_cost(self) -> Balance: + balance = MutableBalance() + for post in self: + if post.cost is None: + balance.add_amount(post.units) + else: + number = post.units.number * post.cost.number + balance.add_amount(data.Amount(number, post.cost.currency)) + return balance + def meta_values(self, key: MetaKey, default: Optional[MetaValue]=None, diff --git a/tests/test_reports_related_postings.py b/tests/test_reports_related_postings.py index fbb484a..13d8ee9 100644 --- a/tests/test_reports_related_postings.py +++ b/tests/test_reports_related_postings.py @@ -135,6 +135,61 @@ def test_iter_with_balance_credit_card(credit_card_cycle): def test_iter_with_balance_two_acccruals(two_accruals_three_payments): check_iter_with_balance(two_accruals_three_payments) +def test_balance_at_cost_mixed(): + txn = testutil.Transaction(postings=[ + ('Expenses:Other', '22'), + ('Expenses:Other', '30', 'EUR', ('1.1',)), + ('Expenses:Other', '40', 'EUR'), + ('Expenses:Other', '50', 'USD', ('1.1', 'EUR')), + ]) + related = core.RelatedPostings(data.Posting.from_txn(txn)) + balance = related.balance_at_cost() + amounts = set(balance.values()) + assert amounts == {testutil.Amount(55, 'USD'), testutil.Amount(95, 'EUR')} + +def test_balance_at_single_currency_cost(): + txn = testutil.Transaction(postings=[ + ('Expenses:Other', '22'), + ('Expenses:Other', '30', 'EUR', ('1.1',)), + ('Expenses:Other', '40', 'GBP', ('1.1',)), + ]) + related = core.RelatedPostings(data.Posting.from_txn(txn)) + balance = related.balance_at_cost() + amounts = set(balance.values()) + assert amounts == {testutil.Amount(99)} + +def test_balance_at_cost_zeroed_out(): + txn = testutil.Transaction(postings=[ + ('Income:Other', '-22'), + ('Assets:Receivable:Accounts', '20', 'EUR', ('1.1',)), + ]) + related = core.RelatedPostings(data.Posting.from_txn(txn)) + balance = related.balance_at_cost() + assert balance.is_zero() + +def test_balance_at_cost_singleton(): + txn = testutil.Transaction(postings=[ + ('Assets:Receivable:Accounts', '20', 'EUR', ('1.1',)), + ]) + related = core.RelatedPostings(data.Posting.from_txn(txn)) + balance = related.balance_at_cost() + amounts = set(balance.values()) + assert amounts == {testutil.Amount(22)} + +def test_balance_at_cost_singleton_without_cost(): + txn = testutil.Transaction(postings=[ + ('Assets:Receivable:Accounts', '20'), + ]) + related = core.RelatedPostings(data.Posting.from_txn(txn)) + balance = related.balance_at_cost() + amounts = set(balance.values()) + assert amounts == {testutil.Amount(20)} + +def test_balance_at_cost_empty(): + related = core.RelatedPostings() + balance = related.balance_at_cost() + assert balance.is_zero() + def test_meta_values_empty(): related = core.RelatedPostings() assert related.meta_values('key') == set()