Files

241 lines
9.7 KiB
Python

"""Tests for src/pdf_filler.py — field dict building and pauschale/plan logic."""
from datetime import date
import pytest
from src.models import AttendanceDate, MonthlyAttendance, Person
from src.pdf_filler import build_field_dict
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def cfg():
return {
"antragsnummer": "SAB-12345",
"ort": "Leipzig",
"template_pdf": "template/sab.pdf",
"output_dir": "output",
"plan_hours_value": "6",
"pdf_field_ort": "unterschrift.ort",
"pdf_field_erstellungsdatum": "unterschrift.datum",
"pdf_pauschale_prefix": "pauschale_5",
"pauschale_present_value": "1",
"excel_year_pattern": "(20\\d{2})",
"excel_name_header": "Name, Vorname",
"excel_name_column": 1,
"excel_seq_column": 0,
"excel_day_start_col": 9,
"excel_day_end_col": 39,
"month_mapping": {"Feb": "02"},
"valid_status_codes": ["K", "KK", "E", "F", "P", "U"],
}
def _make_attendance(*entries) -> MonthlyAttendance:
person = Person(seq_num=1, name="Teilnehmer, Test")
return MonthlyAttendance(
person=person,
year=2026,
month=2,
month_name="Feb",
entries=list(entries),
)
def _present(day: int, hours: float) -> AttendanceDate:
return AttendanceDate(date=date(2026, 2, day), status="X", hours=hours)
def _absent(day: int, status: str) -> AttendanceDate:
return AttendanceDate(date=date(2026, 2, day), status=status, hours=None)
# ---------------------------------------------------------------------------
# Header fields
# ---------------------------------------------------------------------------
class TestHeaderFields:
def test_person_name(self, cfg):
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
assert fields["person.name"] == "Teilnehmer, Test"
def test_monat_jahr(self, cfg):
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
assert fields["monat_jahr"] == "02/2026"
def test_antragsnummer(self, cfg):
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
assert fields["antragsnummer"] == "SAB-12345"
def test_ort(self, cfg):
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
assert fields["unterschrift.ort"] == "Leipzig"
def test_erstellungsdatum(self, cfg):
fields = build_field_dict(_make_attendance(), cfg, date(2026, 3, 27))
assert fields["unterschrift.datum"] == "27.03.2026"
# ---------------------------------------------------------------------------
# Present day (status X)
# ---------------------------------------------------------------------------
class TestPresentDay:
def test_datum_field(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
assert fields["datum.0"] == "03.02.2026"
def test_anwesend_field(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
assert fields["anwesend.0"] == "X"
def test_ist_field_whole_hours(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
assert fields["ist.0"] == "6,00"
def test_ist_field_decimal_hours(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 6.5)), cfg, date(2026, 2, 1))
assert fields["ist.0"] == "6,50"
def test_plan_always_6(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 3.0)), cfg, date(2026, 2, 1))
assert fields["plan.0"] == "6"
def test_pauschale_set(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 6.0)), cfg, date(2026, 2, 1))
assert fields["pauschale_5.0"] == "1"
def test_pauschale_above_threshold(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 7.5)), cfg, date(2026, 2, 1))
assert fields["pauschale_5.0"] == "1"
def test_pauschale_below_threshold(self, cfg):
fields = build_field_dict(_make_attendance(_present(3, 5.0)), cfg, date(2026, 2, 1))
assert fields["pauschale_5.0"] == "0"
# ---------------------------------------------------------------------------
# Absent days (K, KK, E, F, P, U)
# ---------------------------------------------------------------------------
class TestAbsentDay:
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
def test_anwesend_field(self, cfg, status):
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
assert fields["anwesend.0"] == status
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "U"])
def test_ist_empty(self, cfg, status):
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
assert fields["ist.0"] == ""
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "U"])
def test_plan_empty(self, cfg, status):
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
assert fields["plan.0"] == ""
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "U"])
def test_pauschale_empty(self, cfg, status):
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
assert fields["pauschale_5.0"] == "0"
def test_praktikum_sets_fixed_hours(self, cfg):
fields = build_field_dict(_make_attendance(_absent(4, "P")), cfg, date(2026, 2, 1))
assert fields["ist.0"] == "6,00"
assert fields["plan.0"] == "6"
def test_praktikum_sets_pauschale(self, cfg):
fields = build_field_dict(_make_attendance(_absent(4, "P")), cfg, date(2026, 2, 1))
assert fields["pauschale_5.0"] == "1"
# ---------------------------------------------------------------------------
# Multi-row indexing
# ---------------------------------------------------------------------------
class TestMultiRow:
def test_second_entry_indexed_as_1(self, cfg):
entries = [_present(1, 6.0), _absent(2, "K")]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
assert fields["datum.1"] == "02.02.2026"
assert fields["anwesend.1"] == "K"
def test_23_entries_produce_index_0_to_22(self, cfg):
entries = [_present(i, 6.0) for i in range(1, 24)] # 23 entries max
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
# Index 22 should exist
assert "datum.22" in fields
assert "anwesend.22" in fields
# Index 23 must NOT exist
assert "datum.23" not in fields
# ---------------------------------------------------------------------------
# Summary fields
# ---------------------------------------------------------------------------
class TestSummaryFields:
def test_anwesend_sum_counts_qualifying_days(self, cfg):
"""anwesend_sum = number of days where hours >= plan threshold."""
entries = [_present(1, 6.0), _present(2, 7.0), _present(3, 5.0)]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
# Day 1 (6h) and day 2 (7h) qualify, day 3 (5h) does not
assert fields["anwesend_sum"] == "2"
def test_anwesend_sum_counts_praktikum_as_6h(self, cfg):
entries = [_present(1, 6.0), _absent(2, "P"), _present(3, 5.0)]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
assert fields["anwesend_sum"] == "2"
def test_praktikum_increases_pauschale_sum(self, cfg):
entries = [_absent(1, "P")]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
assert fields["pauschale_5_sum"] == "8.60"
assert fields["pauschale_sum"] == "8.60"
def test_pauschale_5_sum_euro(self, cfg):
"""pauschale_5_sum = qualifying days * 8.60 (period decimal for JS)."""
entries = [_present(1, 6.0), _present(2, 7.0), _present(3, 5.0)]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
assert fields["pauschale_5_sum"] == "17.20" # 2 * 8.60
def test_pauschale_sum_equals_5_sum(self, cfg):
entries = [_present(1, 6.0)]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
assert fields["pauschale_sum"] == "8.60"
assert fields["pauschale_25_sum"] == "0.00"
def test_no_qualifying_days(self, cfg):
entries = [_absent(1, "K"), _present(2, 4.0)]
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
assert fields["anwesend_sum"] == "0"
assert fields["pauschale_5_sum"] == "0.00"
assert fields["pauschale_sum"] == "0.00"
def test_empty_attendance(self, cfg):
fields = build_field_dict(_make_attendance(), cfg, date(2026, 2, 1))
assert fields["anwesend_sum"] == "0"
assert fields["pauschale_5_sum"] == "0.00"
# ---------------------------------------------------------------------------
# Hours formatting
# ---------------------------------------------------------------------------
class TestHoursFormatting:
def test_whole_number_no_decimal(self):
entry = AttendanceDate(date=date(2026, 2, 1), status="X", hours=6.0)
assert entry.hours_german() == "6,00"
def test_decimal_comma_separator(self):
entry = AttendanceDate(date=date(2026, 2, 1), status="X", hours=6.5)
assert entry.hours_german() == "6,50"
def test_none_returns_empty(self):
entry = AttendanceDate(date=date(2026, 2, 1), status="K", hours=None)
assert entry.hours_german() == ""