Files @ cc1767a09d1d
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_opening_balances.py - annotation

Brett Smith
fund: Incorporate Equity accounts into Release from Restrictions.

This matches what we do on our Statement of Activities in the
balance sheet report.
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
138928eebf4d
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
138928eebf4d
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
f3c68ff46287
138928eebf4d
f3c68ff46287
f3c68ff46287
f3c68ff46287
cc1767a09d1d
138928eebf4d
cc1767a09d1d
f3c68ff46287
"""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.40'),
        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_RESTRICTED, '-.40', project='Delta'),
        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, 10281),
        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, -900, project='Charlie'),
        FlatPosting.make(A_RESTRICTED, -5, project='Delta'),
        FlatPosting.make(A_UNRESTRICTED, -4180, project='Conservancy'),
    ]