data: Add is_opening_balance_txn function.
This commit is contained in:
parent
d66ba8773f
commit
4eaba1ebf6
3 changed files with 88 additions and 1 deletions
|
@ -21,7 +21,7 @@ throughout Conservancy tools.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import decimal
|
import decimal
|
||||||
import operator
|
import functools
|
||||||
|
|
||||||
from beancount.core import account as bc_account
|
from beancount.core import account as bc_account
|
||||||
from beancount.core import amount as bc_amount
|
from beancount.core import amount as bc_amount
|
||||||
|
@ -281,6 +281,16 @@ def balance_of(txn: Transaction,
|
||||||
currency = weights[0].currency
|
currency = weights[0].currency
|
||||||
return Amount._make((number, currency))
|
return Amount._make((number, currency))
|
||||||
|
|
||||||
|
@functools.lru_cache()
|
||||||
|
def is_opening_balance_txn(txn: Transaction) -> bool:
|
||||||
|
opening_equity = balance_of(txn, Account.is_opening_equity)
|
||||||
|
if not opening_equity.currency:
|
||||||
|
return False
|
||||||
|
rest = balance_of(txn, lambda acct: not acct.is_opening_equity())
|
||||||
|
if not rest.currency:
|
||||||
|
return False
|
||||||
|
return abs(opening_equity.number + rest.number) < decimal.Decimal('.01')
|
||||||
|
|
||||||
def iter_postings(txn: Transaction) -> Iterator[Posting]:
|
def iter_postings(txn: Transaction) -> Iterator[Posting]:
|
||||||
"""Yield an enhanced Posting object for every posting in the transaction"""
|
"""Yield an enhanced Posting object for every posting in the transaction"""
|
||||||
for index, source in enumerate(txn.postings):
|
for index, source in enumerate(txn.postings):
|
||||||
|
|
59
tests/test_data_is_opening_balance_txn.py
Normal file
59
tests/test_data_is_opening_balance_txn.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
"""Test data.is_opening_balance_txn function"""
|
||||||
|
# Copyright © 2020 Brett Smith
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from . import testutil
|
||||||
|
|
||||||
|
from conservancy_beancount import data
|
||||||
|
|
||||||
|
def test_typical_opening():
|
||||||
|
txn = testutil.Transaction.opening_balance()
|
||||||
|
assert data.is_opening_balance_txn(txn)
|
||||||
|
|
||||||
|
def test_multiacct_opening():
|
||||||
|
txn = testutil.Transaction(postings=[
|
||||||
|
('Assets:Receivable:Accounts', 100),
|
||||||
|
(next(testutil.OPENING_EQUITY_ACCOUNTS), -100),
|
||||||
|
('Liabilities:Payable:Accounts', -150),
|
||||||
|
(next(testutil.OPENING_EQUITY_ACCOUNTS), 150),
|
||||||
|
])
|
||||||
|
assert data.is_opening_balance_txn(txn)
|
||||||
|
|
||||||
|
def test_opening_with_fx():
|
||||||
|
txn = testutil.Transaction.opening_balance()
|
||||||
|
equity_post = txn.postings[-1]
|
||||||
|
txn.postings[-1] = equity_post._replace(
|
||||||
|
units=testutil.Amount(equity_post.units.number * Decimal('.9'), 'EUR'),
|
||||||
|
cost=testutil.Cost('1.11111'),
|
||||||
|
)
|
||||||
|
assert data.is_opening_balance_txn(txn)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('acct1,acct2,number', [
|
||||||
|
('Assets:Receivable:Accounts', 'Income:Donations', 100),
|
||||||
|
('Expenses:Other', 'Liabilities:Payable:Accounts', 200),
|
||||||
|
('Expenses:Other', 'Equity:Retained:Costs', 300),
|
||||||
|
# Release from restriction
|
||||||
|
('Equity:Funds:Unrestricted', 'Equity:Funds:Restricted', 400),
|
||||||
|
])
|
||||||
|
def test_not_opening_balance(acct1, acct2, number):
|
||||||
|
txn = testutil.Transaction(postings=[
|
||||||
|
(acct1, number),
|
||||||
|
(acct2, -number),
|
||||||
|
])
|
||||||
|
assert not data.is_opening_balance_txn(txn)
|
|
@ -107,6 +107,12 @@ NON_STRING_METADATA_VALUES = [
|
||||||
Amount(500, None),
|
Amount(500, None),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
OPENING_EQUITY_ACCOUNTS = itertools.cycle([
|
||||||
|
'Equity:Funds:Unrestricted',
|
||||||
|
'Equity:Funds:Restricted',
|
||||||
|
'Equity:OpeningBalance',
|
||||||
|
])
|
||||||
|
|
||||||
class Transaction:
|
class Transaction:
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
date=FY_MID_DATE, flag='*', payee=None,
|
date=FY_MID_DATE, flag='*', payee=None,
|
||||||
|
@ -147,6 +153,18 @@ class Transaction:
|
||||||
posting = arg
|
posting = arg
|
||||||
self.postings.append(posting)
|
self.postings.append(posting)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def opening_balance(cls, acct=None, **txn_meta):
|
||||||
|
if acct is None:
|
||||||
|
acct = next(OPENING_EQUITY_ACCOUNTS)
|
||||||
|
return cls(**txn_meta, postings=[
|
||||||
|
('Assets:Receivable:Accounts', 100),
|
||||||
|
('Assets:Receivable:Loans', 200),
|
||||||
|
('Liabilities:Payable:Accounts', -15),
|
||||||
|
('Liabilities:Payable:Vacation', -25),
|
||||||
|
(acct, -260),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
class TestConfig:
|
class TestConfig:
|
||||||
def __init__(self, *,
|
def __init__(self, *,
|
||||||
|
|
Loading…
Reference in a new issue