176 lines
6.7 KiB
Python
176 lines
6.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"
|
|
|
|
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,5"
|
|
|
|
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"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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", "P", "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", "P", "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", "P", "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"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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"
|
|
|
|
def test_decimal_comma_separator(self):
|
|
entry = AttendanceDate(date=date(2026, 2, 1), status="X", hours=6.5)
|
|
assert entry.hours_german() == "6,5"
|
|
|
|
def test_none_returns_empty(self):
|
|
entry = AttendanceDate(date=date(2026, 2, 1), status="K", hours=None)
|
|
assert entry.hours_german() == ""
|