Files @ 5784068904e8
Branch filter:

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

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.
f3c68ff46287
f3c68ff46287
1b7fdf4f3b00
f3c68ff46287
1b7fdf4f3b00
1b7fdf4f3b00
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
# 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'),
    ]