Files @ e05f55659a89
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_reports_balance_sheet.py

Brett Smith
balance_sheet: Balance only considers post_type for Expenses.

This simplifies the code and slightly optimizes it, since now Balance
won't store and keep re-summing income-type breakdowns that nothing
needs.
"""test_reports_balance_sheet.py - Unit tests for balance sheet report"""
# 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 datetime
import io
import itertools

from decimal import Decimal

import pytest

from . import testutil

import odf.opendocument

from beancount.core.data import Open

from conservancy_beancount import data
from conservancy_beancount.reports import balance_sheet

Fund = balance_sheet.Fund
Period = balance_sheet.Period

clean_account_meta = pytest.fixture(scope='module')(testutil.clean_account_meta)

@pytest.fixture(scope='module')
def income_expense_balances():
    txns = []
    prior_date = datetime.date(2019, 2, 2)
    period_date = datetime.date(2019, 4, 4)
    for (acct, post_type), fund in itertools.product([
            ('Income:Donations', 'Donations'),
            ('Income:Sales', 'RBI'),
            ('Expenses:Postage', 'fundraising'),
            ('Expenses:Postage', 'management'),
            ('Expenses:Postage', 'program'),
            ('Expenses:Services', 'fundraising'),
            ('Expenses:Services', 'program'),
    ], ['Conservancy', 'Alpha']):
        root_acct, _, classification = acct.partition(':')
        try:
            data.Account(acct).meta
        except KeyError:
            data.Account.load_opening(Open(
                {'classification': classification},
                datetime.date(2000, 1, 1),
                acct, None, None,
            ))
        meta = {
            'project': fund,
            f'{root_acct.lower().rstrip("s")}-type': post_type,
        }
        sign = '' if root_acct == 'Expenses' else '-'
        txns.append(testutil.Transaction(date=prior_date, postings=[
            (acct, f'{sign}2.40', meta),
        ]))
        txns.append(testutil.Transaction(date=period_date, postings=[
            (acct, f'{sign}2.60', meta),
        ]))
    return balance_sheet.Balances(
        data.Posting.from_entries(txns),
        datetime.date(2019, 3, 1),
        datetime.date(2020, 3, 1),
    )

@pytest.mark.parametrize('kwargs,expected', [
    ({'account': 'Income:Donations'}, -10),
    ({'account': 'Income'}, -20),
    ({'account': 'Income:Nonexistent'}, None),
    ({'classification': 'Postage'}, 30),
    ({'classification': 'Services'}, 20),
    ({'classification': 'Nonexistent'}, None),
    ({'period': Period.PRIOR, 'account': 'Income'}, '-9.60'),
    ({'period': Period.PERIOD, 'account': 'Expenses'}, 26),
    ({'fund': Fund.RESTRICTED, 'account': 'Income'}, -10),
    ({'fund': Fund.UNRESTRICTED, 'account': 'Expenses'}, 25),
    ({'post_type': 'fundraising'}, 20),
    ({'post_type': 'management'}, 10),
    ({'post_type': 'Nonexistent'}, None),
    ({'period': Period.PRIOR, 'post_type': 'fundraising'}, '9.60'),
    ({'fund': Fund.RESTRICTED, 'post_type': 'program'}, 10),
    ({'period': Period.PRIOR, 'fund': Fund.RESTRICTED, 'post_type': 'program'}, '4.80'),
    ({'period': Period.PERIOD, 'fund': Fund.RESTRICTED, 'post_type': 'ø'}, None),
])
def test_balance_total(income_expense_balances, kwargs, expected):
    actual = income_expense_balances.total(**kwargs)
    if expected is None:
        assert not actual
    else:
        assert actual == {'USD': testutil.Amount(expected)}

def run_main(arglist=[], config=None):
    if config is None:
        config = testutil.TestConfig(books_path=testutil.test_path('books/fund.beancount'))
    stdout = io.BytesIO()
    stderr = io.StringIO()
    retcode = balance_sheet.main(['-O', '-'] + arglist, stdout, stderr, config)
    stdout.seek(0)
    stderr.seek(0)
    return retcode, stdout, stderr

def test_main():
    retcode, stdout, stderr = run_main()
    assert retcode == 0
    assert not stderr.getvalue()
    report = odf.opendocument.load(stdout)
    assert report.spreadsheet.childNodes