363 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			363 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import collections
 | 
						|
import configparser
 | 
						|
import contextlib
 | 
						|
import datetime
 | 
						|
import decimal
 | 
						|
import io
 | 
						|
import pathlib
 | 
						|
 | 
						|
import pytest
 | 
						|
from import2ledger import errors
 | 
						|
from import2ledger.hooks import ledger_entry
 | 
						|
 | 
						|
from . import DATA_DIR, normalize_whitespace
 | 
						|
 | 
						|
DATE = datetime.date(2015, 3, 14)
 | 
						|
 | 
						|
config = configparser.ConfigParser(comment_prefixes='#')
 | 
						|
with pathlib.Path(DATA_DIR, 'templates.ini').open() as conffile:
 | 
						|
    config.read_file(conffile)
 | 
						|
 | 
						|
def template_from(section_name, *args, **kwargs):
 | 
						|
    return ledger_entry.Template(config[section_name]['template'], *args, **kwargs)
 | 
						|
 | 
						|
def template_vars(payee, amount, currency='USD', date=DATE, other_vars=None):
 | 
						|
    call_vars = {
 | 
						|
        'amount': amount,
 | 
						|
        'currency': currency,
 | 
						|
        'date': date,
 | 
						|
        'payee': payee,
 | 
						|
        'ledger template': 'template',
 | 
						|
    }
 | 
						|
    if other_vars is None:
 | 
						|
        return call_vars
 | 
						|
    else:
 | 
						|
        return collections.ChainMap(call_vars, other_vars)
 | 
						|
 | 
						|
def render_lines(render_vars, section_name, *args, **kwargs):
 | 
						|
    tmpl = template_from(section_name, *args, **kwargs)
 | 
						|
    rendered = tmpl.render(render_vars)
 | 
						|
    return [normalize_whitespace(s) for s in rendered.splitlines()]
 | 
						|
 | 
						|
def assert_easy_render(tmpl, entity, amount, currency, expect_date, expect_amt):
 | 
						|
    rendered = tmpl.render(template_vars(entity, amount, currency))
 | 
						|
    lines = [normalize_whitespace(s) for s in rendered.splitlines()]
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "{} {}".format(expect_date, entity),
 | 
						|
        "  Accrued:Accounts Receivable  " + expect_amt,
 | 
						|
        "  Income:Donations  " + expect_amt.replace(amount, "-" + amount),
 | 
						|
    ]
 | 
						|
    assert not tmpl.is_empty()
 | 
						|
 | 
						|
def test_easy_template():
 | 
						|
    tmpl = template_from('Simplest')
 | 
						|
    assert_easy_render(tmpl, 'JJ', '5.99', 'CAD', '2015/03/14', '5.99 CAD')
 | 
						|
 | 
						|
def test_date_formatting():
 | 
						|
    tmpl = template_from('Simplest', date_fmt='%Y-%m-%d')
 | 
						|
    assert_easy_render(tmpl, 'KK', '6.99', 'CAD', '2015-03-14', '6.99 CAD')
 | 
						|
 | 
						|
def test_currency_formatting():
 | 
						|
    tmpl = template_from('Simplest', signed_currencies=['USD'])
 | 
						|
    assert_easy_render(tmpl, 'CC', '7.99', 'USD', '2015/03/14', 'USD7.99')
 | 
						|
 | 
						|
def test_empty_template():
 | 
						|
    tmpl = ledger_entry.Template("\n \n")
 | 
						|
    assert tmpl.render(template_vars('BB', '8.99')) == ''
 | 
						|
    assert tmpl.is_empty()
 | 
						|
 | 
						|
def test_complex_template():
 | 
						|
    render_vars = template_vars('TT', '125.50', other_vars={
 | 
						|
        'entity': 'T-T',
 | 
						|
        'program': 'Spectrum Defense',
 | 
						|
        'txid': 'ABCDEF',
 | 
						|
    })
 | 
						|
    lines = render_lines(
 | 
						|
        render_vars, 'Complex',
 | 
						|
        date_fmt='%Y-%m-%d',
 | 
						|
    )
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015-03-14 TT",
 | 
						|
        "  ;Tag: Value",
 | 
						|
        "  ;TransactionID: ABCDEF",
 | 
						|
        "  Accrued:Accounts Receivable  125.50 USD",
 | 
						|
        "  ;Entity: Supplier",
 | 
						|
        "  Income:Donations:Spectrum Defense  -119.85 USD",
 | 
						|
        "  ;Program: Spectrum Defense",
 | 
						|
        "  ;Entity: T-T",
 | 
						|
        "  Income:Donations:General  -5.65 USD",
 | 
						|
        "  ;Entity: T-T",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_variable_whitespace_cleaned():
 | 
						|
    # There are two critical parts of this to avoid making malformed Ledger
 | 
						|
    # entries:
 | 
						|
    # * Ensure newlines become non-newline whitespace so we don't write
 | 
						|
    #   malformed lines.
 | 
						|
    # * Collapse multiple spaces into one so variables in account names
 | 
						|
    #   can't make malformed account lines by having a premature split
 | 
						|
    #   between the account name and amount.
 | 
						|
    render_vars = template_vars('W\t\tS', '125.50', other_vars={
 | 
						|
        'entity': 'W\fS',
 | 
						|
        'program': 'Spectrum\r\nDefense',
 | 
						|
        'txid': 'ABC\v\tDEF',
 | 
						|
    })
 | 
						|
    lines = render_lines(
 | 
						|
        render_vars, 'Complex',
 | 
						|
        date_fmt='%Y-%m-%d',
 | 
						|
    )
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015-03-14 W S",
 | 
						|
        "  ;Tag: Value",
 | 
						|
        "  ;TransactionID: ABC DEF",
 | 
						|
        "  Accrued:Accounts Receivable  125.50 USD",
 | 
						|
        "  ;Entity: Supplier",
 | 
						|
        "  Income:Donations:Spectrum Defense  -119.85 USD",
 | 
						|
        "  ;Program: Spectrum Defense",
 | 
						|
        "  ;Entity: W S",
 | 
						|
        "  Income:Donations:General  -5.65 USD",
 | 
						|
        "  ;Entity: W S",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_balancing():
 | 
						|
    lines = render_lines(template_vars('FF', '1.01'), 'FiftyFifty')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 FF",
 | 
						|
        "  Accrued:Accounts Receivable  1.01 USD",
 | 
						|
        "  Income:Donations  -0.50 USD",
 | 
						|
        "  Income:Sales  -0.51 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_multivalue():
 | 
						|
    render_vars = template_vars('DD', '150.00', other_vars={
 | 
						|
        'tax': decimal.Decimal('12.50'),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Multivalue')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 DD",
 | 
						|
        "  Expenses:Taxes  12.50 USD",
 | 
						|
        "  ;TaxAuthority: IRS",
 | 
						|
        "  Accrued:Accounts Receivable  137.50 USD",
 | 
						|
        "  Income:RBI  -15.00 USD",
 | 
						|
        "  Income:Donations  -135.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_zeroed_account_skipped():
 | 
						|
    render_vars = template_vars('GG', '110.00', other_vars={
 | 
						|
        'tax': decimal.Decimal(0),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Multivalue')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 GG",
 | 
						|
        "  Accrued:Accounts Receivable  110.00 USD",
 | 
						|
        "  Income:RBI  -11.00 USD",
 | 
						|
        "  Income:Donations  -99.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_zeroed_account_last():
 | 
						|
    render_vars = template_vars('JJ', '90.00', other_vars={
 | 
						|
        'item_sales': decimal.Decimal(0),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Multisplit')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 JJ",
 | 
						|
        "  Assets:Cash  90.00 USD",
 | 
						|
        "  Income:Sales  -90.00 USD",
 | 
						|
        "  ; :NonItem:",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_multiple_postings_same_account():
 | 
						|
    render_vars = template_vars('LL', '80.00', other_vars={
 | 
						|
        'item_sales': decimal.Decimal(30),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Multisplit')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 LL",
 | 
						|
        "  Assets:Cash  80.00 USD",
 | 
						|
        "  Income:Sales  -50.00 USD",
 | 
						|
        "  ; :NonItem:",
 | 
						|
        "  Income:Sales  -30.00 USD",
 | 
						|
        "  ; :Item:",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_custom_payee_line():
 | 
						|
    render_vars = template_vars('ZZ', '10.00', other_vars={
 | 
						|
        'custom_date': datetime.date(2014, 2, 13),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Custom Payee')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2014/02/13  ZZ - Custom",
 | 
						|
        "  Accrued:Accounts Receivable  10.00 USD",
 | 
						|
        "  Income:Donations  -10.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_line1_not_custom_payee():
 | 
						|
    render_vars = template_vars('VV', '15.00', other_vars={
 | 
						|
        'custom_date': datetime.date(2014, 2, 12),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Simplest')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 VV",
 | 
						|
        "  Accrued:Accounts Receivable  15.00 USD",
 | 
						|
        "  Income:Donations  -15.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_only_payee_line_date_is_required():
 | 
						|
    render_vars = template_vars('VY', '17.50', other_vars={
 | 
						|
        'custom_date': datetime.date(2014, 2, 11),
 | 
						|
    })
 | 
						|
    del render_vars['date']
 | 
						|
    lines = render_lines(render_vars, 'Custom Payee')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2014/02/11  VY - Custom",
 | 
						|
        "  Accrued:Accounts Receivable  17.50 USD",
 | 
						|
        "  Income:Donations  -17.50 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_dates_can_be_none_except_payee_line_date():
 | 
						|
    render_vars = template_vars('VZ', '18.00', date=None, other_vars={
 | 
						|
        'custom_date': datetime.date(2014, 2, 10),
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'Custom Payee')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2014/02/10  VZ - Custom",
 | 
						|
        "  Accrued:Accounts Receivable  18.00 USD",
 | 
						|
        "  Income:Donations  -18.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_custom_date_missing():
 | 
						|
    render_vars = template_vars('YY', '20.00')
 | 
						|
    with pytest.raises(errors.UserInputConfigurationError):
 | 
						|
        render_lines(render_vars, 'Custom Payee')
 | 
						|
 | 
						|
def test_custom_date_is_none():
 | 
						|
    render_vars = template_vars('YZ', '25.00', other_vars={
 | 
						|
        'custom_date': None,
 | 
						|
    })
 | 
						|
    with pytest.raises(errors.UserInputConfigurationError):
 | 
						|
        render_lines(render_vars, 'Custom Payee')
 | 
						|
 | 
						|
@pytest.mark.parametrize('amount,expect_fee', [
 | 
						|
    (40, 3),
 | 
						|
    (80, 6),
 | 
						|
])
 | 
						|
def test_conditional(amount, expect_fee):
 | 
						|
    expect_cash = amount - expect_fee
 | 
						|
    amount_s = '{:.02f}'.format(amount)
 | 
						|
    render_vars = template_vars('Buyer', amount_s)
 | 
						|
    lines = render_lines(render_vars, 'Conditional')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 Buyer",
 | 
						|
        "  Assets:Cash  {:.02f} USD".format(expect_cash),
 | 
						|
        "  Expenses:Banking Fees  {:.02f} USD".format(expect_fee),
 | 
						|
        "  Income:Sales  -{} USD".format(amount_s),
 | 
						|
    ]
 | 
						|
 | 
						|
def test_string_conditionals():
 | 
						|
    render_vars = template_vars('MM', '6', other_vars={
 | 
						|
        'false': '',
 | 
						|
        'true': 'true',
 | 
						|
    })
 | 
						|
    lines = render_lines(render_vars, 'StringConditional')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 MM",
 | 
						|
        "  Income:Sales  -1.00 USD",
 | 
						|
        "  Income:Sales  -2.00 USD",
 | 
						|
        "  Income:Sales  -3.00 USD",
 | 
						|
        "  Assets:Cash  6.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_self_balanced():
 | 
						|
    # The amount needs to be different than what's in the template.
 | 
						|
    render_vars = template_vars('NN', '0')
 | 
						|
    lines = render_lines(render_vars, 'SelfBalanced')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015/03/14 NN",
 | 
						|
        "  Income:Sales  -5.00 USD",
 | 
						|
        "  Assets:Cash  5.00 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
@pytest.mark.parametrize('amount_expr', [
 | 
						|
    '',
 | 
						|
    'name',
 | 
						|
    '-',
 | 
						|
    '()',
 | 
						|
    '+()',
 | 
						|
    '{}',
 | 
						|
    '{{}}',
 | 
						|
    '{()}',
 | 
						|
    '{name',
 | 
						|
    'name}',
 | 
						|
    '{42}',
 | 
						|
    '(5).real',
 | 
						|
    '{amount.real}',
 | 
						|
    '{amount.is_nan()}',
 | 
						|
    '{Decimal}',
 | 
						|
    '{FOO}',
 | 
						|
])
 | 
						|
def test_bad_amount_expression(amount_expr):
 | 
						|
    with pytest.raises(errors.UserInputError):
 | 
						|
        ledger_entry.Template(" Income  " + amount_expr)
 | 
						|
 | 
						|
class Config:
 | 
						|
    def __init__(self, use_section):
 | 
						|
        self.section_name = use_section
 | 
						|
        self.stdout = io.StringIO()
 | 
						|
 | 
						|
    @contextlib.contextmanager
 | 
						|
    def open_output_file(self):
 | 
						|
        yield self.stdout
 | 
						|
 | 
						|
    def get_section(self, name=None):
 | 
						|
        return config[self.section_name]
 | 
						|
 | 
						|
 | 
						|
def run_hook(entry_data, config_section):
 | 
						|
    hook_config = Config(config_section)
 | 
						|
    hook = ledger_entry.LedgerEntryHook(hook_config)
 | 
						|
    assert hook.run(entry_data) is None
 | 
						|
    stdout = hook_config.stdout.getvalue()
 | 
						|
    return normalize_whitespace(stdout).splitlines()
 | 
						|
 | 
						|
def test_hook_renders_template():
 | 
						|
    entry_data = template_vars('BB', '0.99')
 | 
						|
    lines = run_hook(entry_data, 'Simplest')
 | 
						|
    assert lines == [
 | 
						|
        "",
 | 
						|
        "2015-03-14 BB",
 | 
						|
        "  Accrued:Accounts Receivable  0.99 USD",
 | 
						|
        "  Income:Donations  -0.99 USD",
 | 
						|
    ]
 | 
						|
 | 
						|
def test_hook_handles_empty_template():
 | 
						|
    entry_data = template_vars('CC', 1)
 | 
						|
    assert not run_hook(entry_data, 'Empty')
 | 
						|
 | 
						|
def test_hook_handles_template_undefined():
 | 
						|
    entry_data = template_vars('DD', 1)
 | 
						|
    assert not run_hook(entry_data, 'Nonexistent')
 | 
						|
 | 
						|
def test_string_value_is_user_error():
 | 
						|
    entry_data = template_vars('EE', 1)
 | 
						|
    with pytest.raises(errors.UserInputConfigurationError):
 | 
						|
        run_hook(entry_data, 'NondecimalWord')
 | 
						|
 | 
						|
def test_string_variable_is_user_error():
 | 
						|
    entry_data = template_vars('FF', 1)
 | 
						|
    with pytest.raises(errors.UserInputConfigurationError):
 | 
						|
        run_hook(entry_data, 'NondecimalVariable')
 |