151 lines
5.6 KiB
Python
151 lines
5.6 KiB
Python
|
"""test_tools_opening_balances.py - Unit tests for opening balance generation"""
|
||
|
# 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/>.
|
||
|
|
||
|
import collections
|
||
|
import copy
|
||
|
import datetime
|
||
|
import io
|
||
|
|
||
|
import pytest
|
||
|
|
||
|
from . import testutil
|
||
|
|
||
|
from beancount import loader as bc_loader
|
||
|
from conservancy_beancount.tools import opening_balances as openbalmod
|
||
|
|
||
|
from decimal import Decimal
|
||
|
from typing import NamedTuple, Optional
|
||
|
|
||
|
A_CHECKING = 'Assets:Checking'
|
||
|
A_EUR = 'Assets:EUR'
|
||
|
A_PREPAID = 'Assets:Prepaid:Expenses'
|
||
|
A_RECEIVABLE = 'Assets:Receivable:Accounts'
|
||
|
A_RESTRICTED = 'Equity:Funds:Restricted'
|
||
|
A_UNRESTRICTED = 'Equity:Funds:Unrestricted'
|
||
|
A_CURRCONV = 'Equity:Realized:CurrencyConversion'
|
||
|
A_CREDITCARD = 'Liabilities:CreditCard'
|
||
|
A_PAYABLE = 'Liabilities:Payable:Accounts'
|
||
|
A_UNEARNED = 'Liabilities:UnearnedIncome'
|
||
|
|
||
|
class FlatPosting(NamedTuple):
|
||
|
account: str
|
||
|
units_number: Decimal
|
||
|
units_currency: str
|
||
|
cost_number: Optional[Decimal]
|
||
|
cost_currency: Optional[str]
|
||
|
cost_date: Optional[datetime.date]
|
||
|
project: Optional[str]
|
||
|
|
||
|
def __repr__(self):
|
||
|
cost_s = f' {{{self.cost_number} {self.cost_currency}, {self.cost_date}}}'
|
||
|
if cost_s == ' {None None, None}':
|
||
|
cost_s = ''
|
||
|
return f'<{self.account} {self.units_number} {self.units_currency}{cost_s}>'
|
||
|
|
||
|
@classmethod
|
||
|
def make(cls,
|
||
|
account,
|
||
|
units_number,
|
||
|
units_currency='USD',
|
||
|
cost_number=None,
|
||
|
cost_date=None,
|
||
|
project=None,
|
||
|
cost_currency=None,
|
||
|
):
|
||
|
if cost_number is not None:
|
||
|
cost_number = Decimal(cost_number)
|
||
|
if cost_currency is None:
|
||
|
cost_currency = 'USD'
|
||
|
if isinstance(cost_date, str):
|
||
|
cost_date = datetime.datetime.strptime(cost_date, '%Y-%m-%d').date()
|
||
|
return cls(account, Decimal(units_number), units_currency,
|
||
|
cost_number, cost_currency, cost_date, project)
|
||
|
|
||
|
@classmethod
|
||
|
def from_beancount(cls, posting):
|
||
|
units_number, units_currency = posting.units
|
||
|
if posting.cost is None:
|
||
|
cost_number = cost_currency = cost_date = None
|
||
|
else:
|
||
|
cost_number, cost_currency, cost_date, _ = posting.cost
|
||
|
try:
|
||
|
project = posting.meta['project']
|
||
|
except (AttributeError, KeyError):
|
||
|
project = None
|
||
|
return cls(posting.account, units_number, units_currency,
|
||
|
cost_number, cost_currency, cost_date, project)
|
||
|
|
||
|
@classmethod
|
||
|
def from_output(cls, output):
|
||
|
entries, _, _ = bc_loader.load_string(output.read())
|
||
|
return (cls.from_beancount(post) for post in entries[-1].postings)
|
||
|
|
||
|
|
||
|
def run_main(arglist, config=None):
|
||
|
if config is None:
|
||
|
config = testutil.TestConfig(
|
||
|
books_path=testutil.test_path('books/fund.beancount'),
|
||
|
)
|
||
|
output = io.StringIO()
|
||
|
errors = io.StringIO()
|
||
|
retcode = openbalmod.main(arglist, output, errors, config)
|
||
|
output.seek(0)
|
||
|
return retcode, output, errors
|
||
|
|
||
|
@pytest.mark.parametrize('arg', ['2018', '2018-03-01'])
|
||
|
def test_2018_opening(arg):
|
||
|
retcode, output, errors = run_main([arg])
|
||
|
assert not errors.getvalue()
|
||
|
assert retcode == 0
|
||
|
assert list(FlatPosting.from_output(output)) == [
|
||
|
FlatPosting.make(A_CHECKING, 10000),
|
||
|
FlatPosting.make(A_RESTRICTED, -3000, project='Alpha'),
|
||
|
FlatPosting.make(A_RESTRICTED, -2000, project='Bravo'),
|
||
|
FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'),
|
||
|
FlatPosting.make(A_UNRESTRICTED, -4000, project='Conservancy'),
|
||
|
]
|
||
|
|
||
|
@pytest.mark.parametrize('arg', ['2019', '2019-03-01'])
|
||
|
def test_2019_opening(arg):
|
||
|
retcode, output, errors = run_main([arg])
|
||
|
assert not errors.getvalue()
|
||
|
assert retcode == 0
|
||
|
assert list(FlatPosting.from_output(output)) == [
|
||
|
FlatPosting.make(A_CHECKING, 10050),
|
||
|
FlatPosting.make(A_PREPAID, 20, project='Alpha'),
|
||
|
FlatPosting.make(A_RECEIVABLE, 32, 'EUR', '1.25', '2018-03-03', 'Conservancy'),
|
||
|
FlatPosting.make(A_RESTRICTED, -3060, project='Alpha'),
|
||
|
FlatPosting.make(A_RESTRICTED, -1980, project='Bravo'),
|
||
|
FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'),
|
||
|
FlatPosting.make(A_UNRESTRICTED, -4036, project='Conservancy'),
|
||
|
FlatPosting.make(A_PAYABLE, -4, project='Conservancy'),
|
||
|
FlatPosting.make(A_UNEARNED, -30, project='Alpha'),
|
||
|
]
|
||
|
|
||
|
@pytest.mark.parametrize('arg', ['2020', '2020-12-31'])
|
||
|
def test_2020_opening(arg):
|
||
|
retcode, output, errors = run_main([arg])
|
||
|
assert not errors.getvalue()
|
||
|
assert retcode == 0
|
||
|
assert list(FlatPosting.from_output(output)) == [
|
||
|
FlatPosting.make(A_CHECKING, 10276),
|
||
|
FlatPosting.make(A_EUR, 32, 'EUR', '1.5', '2019-03-03'),
|
||
|
FlatPosting.make(A_RESTRICTED, -3064, project='Alpha'),
|
||
|
FlatPosting.make(A_RESTRICTED, -2180, project='Bravo'),
|
||
|
FlatPosting.make(A_RESTRICTED, -1000, project='Charlie'),
|
||
|
FlatPosting.make(A_UNRESTRICTED, -4080, project='Conservancy'),
|
||
|
]
|