Files @ 5784068904e8
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_pdfforms_fields.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.
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
391fde5447de
391fde5447de
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
6a3d64ff2250
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
6a3d64ff2250
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
6a3d64ff2250
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
391fde5447de
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
13c66e8ce296
0045d8d03205
0045d8d03205
0045d8d03205
0045d8d03205
0045d8d03205
0045d8d03205
0045d8d03205
0045d8d03205
"""test_pdfforms_fields.py - Unit tests for PDF forms manipulation"""
# 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 codecs
import itertools

import pytest

from decimal import Decimal

from pdfminer.psparser import PSLiteral

from conservancy_beancount.pdfforms import fields as fieldsmod

def field_source(
        name=None,
        value=None,
        field_type=None,
        flags=None,
        parent=None,
        kids=None,
        *,
        literal=None,
):
    retval = {}
    if isinstance(name, str):
        retval['T'] = name.encode('ascii')
    elif name is not None:
        retval['T'] = name
    if value is not None:
        if literal is None:
            literal = field_type and field_type != 'Tx'
        if literal:
            value = PSLiteral(value)
        retval['V'] = value
    if field_type is not None:
        retval['FT'] = PSLiteral(field_type)
    if flags is not None:
        retval['Ff'] = flags
    if parent is not None:
        retval['Parent'] = parent
    if kids is not None:
        retval['Kids'] = list(kids)
    return retval

def appearance_states(*names):
    return {key: object() for key in names if key is not None}

def test_empty_field():
    source = field_source()
    field = fieldsmod.FormField(source)
    assert not field.name()
    assert field.value() is None
    assert field.parent() is None
    assert not list(field.kids())
    assert field.flags() == 0
    assert field.is_terminal()
    with pytest.raises(ValueError):
        field.field_type()

def test_text_field_base():
    source = field_source(b's', b'string of text', 'Tx')
    field = fieldsmod.FormField(source)
    assert field.field_type() is fieldsmod.FieldType.Text
    assert field.name() == 's'
    assert field.value() == b'string of text'

@pytest.mark.parametrize('value', ['Off', 'Yes', 'On'])
def test_checkbox_field_base(value):
    source = field_source(b'cb', value, 'Btn', literal=True)
    field = fieldsmod.FormField(source)
    assert field.field_type() is fieldsmod.FieldType.Button
    assert field.name() == 'cb'
    assert field.value().name == value

@pytest.mark.parametrize('flags', range(4))
def test_readonly_flag(flags):
    source = field_source(flags=flags)
    field = fieldsmod.FormField(source)
    assert field.flags() == flags
    assert field.is_readonly() == flags % 2

@pytest.mark.parametrize('kid_count', range(3))
def test_kids(kid_count):
    kids = [field_source(f'kid{n}', field_type='Ch') for n in range(kid_count)]
    source = field_source(kids=iter(kids))
    field = fieldsmod.FormField(source)
    got_kids = list(field.kids())
    assert len(got_kids) == len(kids)
    assert field.is_terminal() == (not kids)
    for actual, expected in zip(got_kids, kids):
        assert actual.name() == expected['T'].decode('ascii')

def test_kids_by_type():
    kids = [field_source(field_type='Tx'), field_source(field_type='Btn')]
    source = field_source('topform', kids=iter(kids))
    actual = fieldsmod.FormField.by_type(source).kids()
    assert isinstance(next(actual), fieldsmod.TextField)
    assert isinstance(next(actual), fieldsmod.CheckboxField)
    assert next(actual, None) is None

def test_inheritance():
    parent_source = field_source(b'parent', 'parent value', 'Tx', 17)
    kid_source = field_source('kid', parent=parent_source)
    parent_source['Kids'] = [kid_source]
    field = fieldsmod.FormField(kid_source)
    parent = field.parent()
    assert parent is not None
    assert parent.name() == 'parent'
    assert not parent.is_terminal()
    assert field.is_terminal()
    assert field.name() == 'kid'
    assert field.field_type() is fieldsmod.FieldType.Text
    assert field.value() == 'parent value'
    assert field.flags() == 17
    assert not list(field.kids())

@pytest.mark.parametrize('field_type,value', [
    ('Tx', b'new value'),
    ('Btn', PSLiteral('Yes')),
])
def test_set_value(field_type, value):
    source = field_source(field_type=field_type)
    field = fieldsmod.FormField(source)
    assert field.value() is None
    field.set_value(value)
    assert field.value() == value

@pytest.mark.parametrize('field_type,expected', [
    ('Tx', fieldsmod.TextField),
    ('Btn', fieldsmod.CheckboxField),
])
def test_by_type(field_type, expected):
    source = field_source(field_type=field_type)
    field = fieldsmod.FormField.by_type(source)
    assert isinstance(field, expected)

def test_container_by_type():
    kids = [field_source(field_type='Tx'), field_source(field_type='Btn')]
    source = field_source('topform', kids=iter(kids))
    field = fieldsmod.FormField.by_type(source)
    assert isinstance(field, fieldsmod.FormField)

@pytest.mark.parametrize('flag', [
    # If you add dedicated classes for these types of buttons, you can remove
    # their test cases.
    fieldsmod.FieldFlags.Radio,
    fieldsmod.FieldFlags.Pushbutton,
])
def test_unsupported_button_by_type(flag):
    source = field_source(field_type='Btn', flags=flag)
    field = fieldsmod.FormField.by_type(source)
    assert type(field) is fieldsmod.FormField

@pytest.mark.parametrize('field_type', [
    # If you add dedicated classes for these types of fields, you can remove
    # their test cases.
    'Ch',
    'Sig',
])
def test_unsupported_field_by_type(field_type):
    source = field_source(field_type=field_type)
    field = fieldsmod.FormField.by_type(source)
    assert type(field) is fieldsmod.FormField

@pytest.mark.parametrize('value', [None, 'Off', 'Yes'])
def test_checkbox_value(value):
    source = field_source('cb', value, 'Btn', literal=True)
    field = fieldsmod.CheckboxField(source)
    assert field.value() == (value and value == 'Yes')

@pytest.mark.parametrize('value,expected', [
    (None, None),
    (False, 'Off'),
    (True, 'Yes'),
])
def test_checkbox_set_value(value, expected):
    source = field_source('cb', field_type='Btn')
    field = fieldsmod.CheckboxField(source)
    field.set_value(value)
    actual = fieldsmod.FormField.value(field)
    if expected is None:
        assert actual is None
    else:
        assert actual.name == expected

@pytest.mark.parametrize('on_key,off_key', itertools.product(
    ['1', '2', 'On', 'Yes'],
    ['Off', None],
))
def test_checkbox_options(on_key, off_key):
    source = field_source('cb', field_type='Btn')
    source['AP'] = {'N': appearance_states(on_key, off_key)}
    field = fieldsmod.CheckboxField(source)
    assert field.options() == [on_key, 'Off']

def test_checkbox_options_yes_no():
    # I'm not sure this is actually allowed under the spec, but…
    expected = ['Yes', 'No']
    source = field_source('cb', field_type='Btn')
    source['AP'] = {'N': appearance_states(*expected)}
    field = fieldsmod.CheckboxField(source)
    assert field.options() == expected

@pytest.mark.parametrize('on_key,off_key,set_value', itertools.product(
    ['1', '2', 'On', 'Yes'],
    ['Off', None],
    [True, False, None],
))
def test_checkbox_set_custom_value(on_key, off_key, set_value):
    source = field_source('cb', field_type='Btn')
    source['AP'] = {'N': appearance_states(on_key, off_key)}
    field = fieldsmod.CheckboxField(source)
    field.set_value(set_value)
    actual = fieldsmod.FormField.value(field)
    if set_value is None:
        assert actual is None
    elif set_value:
        assert actual.name == (on_key or 'Yes')
    else:
        assert actual.name == 'Off'

@pytest.mark.parametrize('encoding,prefix', [
    ('ascii', b''),
    ('utf-16be', codecs.BOM_UTF16_BE),
])
def test_text_value(encoding, prefix):
    expected = f'{encoding} encoding test'
    value = prefix + expected.encode(encoding)
    source = field_source('t', value, 'Tx')
    field = fieldsmod.TextField(source)
    assert field.value() == expected

def test_text_value_none():
    source = field_source(field_type='Tx')
    assert fieldsmod.TextField(source).value() is None

@pytest.mark.parametrize('text,bprefix', [
    ('ASCII test', b''),
    ('UTF—16 test', codecs.BOM_UTF16_BE),
])
def test_text_set_value(text, bprefix):
    source = field_source(field_type='Tx')
    field = fieldsmod.TextField(source)
    field.set_value(text)
    assert field.value() == text
    actual = fieldsmod.FormField.value(field)
    assert actual == bprefix + text.encode('utf-16be' if bprefix else 'ascii')

@pytest.mark.parametrize('expected', [
    '0',
    '32',
    '32.45',
    '32,768',
    '32,768.95',
])
def test_text_set_value_numeric(expected):
    num_s = expected.replace(',', '')
    field = fieldsmod.TextField({})
    num_types = [Decimal, float]
    if '.' not in expected:
        num_types.append(int)
    for num_type in num_types:
        field.set_value(num_type(num_s))
        assert field.value() == expected
        field.set_value(None)

def test_text_set_value_none():
    source = field_source('t', b'set None test', 'Tx')
    field = fieldsmod.TextField(source)
    field.set_value(None)
    assert fieldsmod.FormField.value(field) is None

def test_empty_as_filled_fdf():
    source = field_source()
    field = fieldsmod.FormField(source)
    assert field.as_filled_fdf() == {}

@pytest.mark.parametrize('field_type,field_class,set_value', [
    ('Btn', fieldsmod.CheckboxField, True),
    ('Btn', fieldsmod.CheckboxField, False),
    ('Ch', fieldsmod.FormField, None),
    ('Tx', fieldsmod.TextField, 'export test'),
    ('Tx', fieldsmod.TextField, 'UTF—16 export'),
])
def test_as_filled_fdf_after_set_value(field_type, field_class, set_value):
    source = field_source(field_type, field_type=field_type)
    field = field_class(source)
    field.set_value(set_value)
    actual = field.as_filled_fdf()
    assert actual['T'] == field_type
    expect_len = 2
    if set_value is None:
        assert 'V' not in actual
        expect_len = 1
    elif field_class is fieldsmod.CheckboxField:
        assert actual['V'].name == ('Yes' if set_value else 'Off')
    else:
        assert actual['V'] == set_value
    assert len(actual) == expect_len

@pytest.mark.parametrize('field_type,expected', [
    ('Btn', None),
    ('Tx', ''),
])
def test_as_filled_fdf_default_value(field_type, expected):
    source = field_source(field_type=field_type)
    field = fieldsmod.FormField.by_type(source)
    actual = field.as_filled_fdf()
    assert actual.get('V') == expected

def test_as_filled_fdf_recursion():
    buttons = [field_source(f'bt{n}', field_type='Btn') for n in range(1, 3)]
    pair = field_source('Buttons', kids=iter(buttons))
    text = field_source('tx', field_type='Tx')
    source = field_source('topform', kids=[text, pair])
    field = fieldsmod.FormField(source)
    actual = field.as_filled_fdf()
    assert actual['T'] == 'topform'
    assert 'V' not in actual
    actual = iter(actual['Kids'])
    assert next(actual)['T'] == 'tx'
    actual = next(actual)
    assert actual['T'] == 'Buttons'
    assert 'V' not in actual
    actual = iter(actual['Kids'])
    assert next(actual)['T'] == 'bt1'
    assert next(actual)['T'] == 'bt2'
    assert next(actual, None) is None

@pytest.mark.parametrize('name,value,field_type', [
    (None, None, None),
    ('mt', 'mapping text', 'Tx'),
    ('mb', 'Yes', 'Btn'),
])
def test_simple_as_mapping(name, value, field_type):
    source = field_source(name, value, field_type)
    field = fieldsmod.FormField(source)
    actual = field.as_mapping()
    key, mapped = next(actual)
    assert key == (name or '')
    assert mapped is field
    assert next(actual, None) is None

def test_recursive_as_mapping():
    btn_kids = [field_source(f'btn{n}', field_type='Btn') for n in range(1, 3)]
    buttons = field_source('buttons', kids=iter(btn_kids))
    text_kids = [field_source(f'tx{n}', field_type='Tx') for n in range(1, 3)]
    texts = field_source('texts', kids=iter(text_kids))
    source = field_source('root', kids=[texts, buttons])
    root_field = fieldsmod.FormField(source)
    actual = root_field.as_mapping()
    for expected_key in [
            'root',
            'root.texts',
            'root.texts.tx1',
            'root.texts.tx2',
            'root.buttons',
            'root.buttons.btn1',
            'root.buttons.btn2',
    ]:
        key, field = next(actual)
        assert key == expected_key
        _, _, expected_name = expected_key.rpartition('.')
        assert field.name() == expected_name
    assert next(actual, None) is None

def test_add_kid():
    parent = fieldsmod.FormField(field_source('parent'))
    kid = fieldsmod.FormField(field_source('kid'))
    parent.add_kid(kid)
    actual, = parent.kids()
    assert actual.name() == 'kid'
    assert actual.parent().name() == 'parent'