"""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 . 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'), ]