Files @ 5784068904e8
Branch filter:

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

bkuhn
payroll-type — US:403b:Employee:Roth — needed separate since taxable

Since Roth contributions are taxable, there are some reports that
need to include these amounts in total salary (i.e., when running a
report that seeks to show total taxable income for an employee). As
such, we need a `payroll-type` specifically for Roth 403(b)
contributions.
"""test_tools_opening_balances.py - Unit tests for opening balance generation"""
# Copyright © 2020  Brett Smith
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
#
# Full copyright and licensing details can be found at toplevel file
# LICENSE.txt in the repository.

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