Some checks failed
ETL-QS / etl-tests (push) Failing after 44s
- Pfad-Defaults im DAG auf das Repo-Checkout /opt/airflow/git/current umgestellt (include-Skripte + etl_cache) und Ziel-DB auf analytics_pg_duckdb festgelegt - tests/: Fixture-Generator (>=4 Dateien je Quell-Ordner mit Dubletten/ Edge-Cases) und End-to-End-Runner mit 45 Pruefungen gegen erwartete Ergebnisse, inkl. README - .forgejo/workflows: CI laeuft die ETL-QS bei Aenderungen an ETL-Skript, tests/ oder Geo-Referenz (python:3.12-Container, runs-on docker) - .gitignore: .venv_test/ und generierte tests/fixtures/ ausgeschlossen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
273 lines
11 KiB
Python
273 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
generate_fixtures.py
|
|
--------------------
|
|
Erzeugt deterministische Test-Eingabedaten für den HVB-ETL-Prozess
|
|
(Skript include/02_etl_angebote_zuschlaege.py).
|
|
|
|
Pro Quell-Ordner werden MINDESTENS 4 Excel-Dateien erzeugt, die gezielt
|
|
folgende Fälle abdecken (vollständige Beschreibung in tests/README.md):
|
|
|
|
taifun_export/ - dieselbe Angebotsnummer in mehreren Dateien
|
|
(jüngster Export-Stand gewinnt), Standort- vs.
|
|
Kundenadresse, Auslands-Angebot, Projektnummer-Status.
|
|
pflege/ - UNION mehrerer Status-Historien, exakte Dubletten,
|
|
Tie-Break per Status-Rang, Waisen-Status (ohne Angebot).
|
|
archiv/ - Alt-Angebote ergänzen den Export; bereits im Export
|
|
vorhandene Nummern werden ignoriert; Datei-Dubletten.
|
|
zuschlaege/ - zwei Blätter (Detail/Kompakt) mit Kopfzeile in Zeile 4,
|
|
Dedup je (Zuschlags-Nr, Flurstück), PLZ ohne Geo-Treffer.
|
|
|
|
Aufruf: python tests/generate_fixtures.py
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
|
|
BASIS = Path(__file__).parent
|
|
INPUT = BASIS / "fixtures" / "input"
|
|
|
|
# Reale PLZ aus etl_cache/geo_plz_koordinaten.csv (haben Koordinaten):
|
|
# 10115 Berlin | 20095 Hamburg | 50667 Köln | 80331 München
|
|
# 04109 Leipzig (führende Null!) | 99999 = bewusst OHNE Geo-Treffer
|
|
|
|
EXPORT_COLS = [
|
|
"Nummer", "Datum", "Kunde", "Debitor", "Beschreibung", "Bearbeiter",
|
|
"Status", "Projekt", "Gültig bis",
|
|
"Kunden-Adresse: PLZ, Ort", "Kunden-Adresse: Straße, Nr.",
|
|
"Kunden-Adresse: Land (ISO-Ländercode)", "Kunden-Adresse: Telefon",
|
|
"Kunden-Adresse: Mobiltelefon", "Standort-Adresse: PLZ, Ort",
|
|
"Wiedervorlage: Datum",
|
|
]
|
|
|
|
|
|
def _exp(nummer, datum, kunde, debitor, beschreibung, projekt="",
|
|
kunden_plz_ort="", land="DE", standort_plz_ort=""):
|
|
return {
|
|
"Nummer": nummer, "Datum": datum, "Kunde": kunde, "Debitor": debitor,
|
|
"Beschreibung": beschreibung, "Bearbeiter": "Schmidt",
|
|
"Status": "", "Projekt": projekt, "Gültig bis": "2099-12-31",
|
|
"Kunden-Adresse: PLZ, Ort": kunden_plz_ort,
|
|
"Kunden-Adresse: Straße, Nr.": "Teststr. 1",
|
|
"Kunden-Adresse: Land (ISO-Ländercode)": land,
|
|
"Kunden-Adresse: Telefon": "030-123", "Kunden-Adresse: Mobiltelefon": "",
|
|
"Standort-Adresse: PLZ, Ort": standort_plz_ort,
|
|
"Wiedervorlage: Datum": "",
|
|
}
|
|
|
|
|
|
def schreibe_export():
|
|
ordner = INPUT / "taifun_export"
|
|
ordner.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Datei 1 (ältester Stand, siehe _dateistand.csv): A-1001 v0 ("ALT")
|
|
df1 = pd.DataFrame([
|
|
_exp("A-1001", "2023-12-01", "Müller ALT GmbH", 70001,
|
|
"Altanlage 100 m³", kunden_plz_ort="10115 Berlin"),
|
|
])
|
|
# Datei 2: A-1001 v1, A-1002 (mit echter Projektnummer + Pflege -> Konflikt)
|
|
df2 = pd.DataFrame([
|
|
_exp("A-1001", "2024-01-10", "Müller GmbH", 70001,
|
|
"Pufferspeicher 800 m³", kunden_plz_ort="10115 Berlin"),
|
|
_exp("A-1002", "2024-01-20", "Bauer AG", 70002, "Heizung 500 m³",
|
|
projekt="P-12345", kunden_plz_ort="20095 Hamburg"),
|
|
])
|
|
# Datei 3: A-1003 (Pflege Auftrag), A-1004 (Projektnr ohne Pflege -> Auftrag)
|
|
df3 = pd.DataFrame([
|
|
_exp("A-1003", "2024-02-05", "Klein KG", 70003, "Solaranlage",
|
|
projekt="Auftrag verloren", kunden_plz_ort="50667 Köln"),
|
|
_exp("A-1004", "2024-02-12", "Lang GmbH", 70004, "Anlage 2000 m³",
|
|
projekt="P-67890", kunden_plz_ort="", standort_plz_ort="80331 München"),
|
|
])
|
|
# Datei 4 (jüngster Stand): A-1001 v2 (gewinnt!), A-1005 (gleicher Kunde
|
|
# wie A-1001 -> Kunden-Dedup), A-1006 (Ausland + Abbruch-Projekttext)
|
|
df4 = pd.DataFrame([
|
|
_exp("A-1001", "2024-03-02", "Müller GmbH", 70001,
|
|
"Pufferspeicher 1000 m³ neu", kunden_plz_ort="10115 Berlin"),
|
|
_exp("A-1005", "2024-03-15", "Müller GmbH", 70001,
|
|
"Erweiterung 1200 m³", kunden_plz_ort="10115 Berlin"),
|
|
_exp("A-1006", "2024-03-20", "Swiss AG", 70006, "Anlage CH",
|
|
projekt="kein Interesse", kunden_plz_ort="1010 Wien", land="CH"),
|
|
])
|
|
|
|
for name, df in [
|
|
("export_00_dup.xlsx", df1),
|
|
("export_2024_01.xlsx", df2),
|
|
("export_2024_02.xlsx", df3),
|
|
("export_2024_03.xlsx", df4),
|
|
]:
|
|
df[EXPORT_COLS].to_excel(ordner / name, sheet_name="Tabelle 1",
|
|
index=False)
|
|
|
|
# _dateistand.csv: legt die Reihenfolge "jüngster Export gewinnt" fest.
|
|
# (Im Echtbetrieb schreibt der DAG diese Datei aus den WebDAV-mtimes.)
|
|
stand = pd.DataFrame([
|
|
{"datei": "export_00_dup.xlsx", "stand_epoch": 500.0},
|
|
{"datei": "export_2024_01.xlsx", "stand_epoch": 2000.0},
|
|
{"datei": "export_2024_02.xlsx", "stand_epoch": 3000.0},
|
|
{"datei": "export_2024_03.xlsx", "stand_epoch": 4000.0},
|
|
])
|
|
stand.to_csv(INPUT / "_dateistand.csv", index=False)
|
|
print(f"taifun_export: 4 Dateien + _dateistand.csv")
|
|
|
|
|
|
def _pflege(nummer, datum, status, netto=None, kontaktart="Telefon",
|
|
zustaendig="Meier"):
|
|
return {
|
|
"Nummer": nummer, "Status-Datum": datum, "Status": status,
|
|
"Netto (EUR)": netto, "Kontaktart": kontaktart, "Zuständig": zustaendig,
|
|
"Wettbewerber": "", "Abbruchgrund": "", "Wiedervorlage": "",
|
|
"Bemerkungen": "",
|
|
}
|
|
|
|
|
|
def schreibe_pflege():
|
|
ordner = INPUT / "pflege"
|
|
ordner.mkdir(parents=True, exist_ok=True)
|
|
|
|
df1 = pd.DataFrame([
|
|
_pflege("A-1001", "2024-01-15", "Kontakt/Lead", 0),
|
|
_pflege("A-1001", "2024-02-20", "Angebot", 50000),
|
|
_pflege("A-1002", "2024-02-01", "Angebot", 30000),
|
|
])
|
|
df2 = pd.DataFrame([
|
|
_pflege("A-1001", "2024-03-05", "Verhandlung", 55000),
|
|
_pflege("A-1003", "2024-02-10", "Auftrag", 80000),
|
|
])
|
|
# Datei 3: exakte Dublette von A-1003 (wird entfernt) + Tie-Break-Test:
|
|
# A-1001 am selben Datum wie "Verhandlung", aber niedrigerer Rang -> verliert.
|
|
df3 = pd.DataFrame([
|
|
_pflege("A-1003", "2024-02-10", "Auftrag", 80000),
|
|
_pflege("A-1001", "2024-03-05", "Angebot", 52000),
|
|
])
|
|
# Datei 4: Waisen-Status ohne zugehöriges Angebot (nur in Historie sichtbar)
|
|
df4 = pd.DataFrame([
|
|
_pflege("A-9999", "2024-01-01", "Angebot", 9999),
|
|
])
|
|
|
|
for name, df in [
|
|
("pflege_2024_01.xlsx", df1),
|
|
("pflege_2024_02.xlsx", df2),
|
|
("pflege_2024_03_dup.xlsx", df3),
|
|
("pflege_2024_04_waise.xlsx", df4),
|
|
]:
|
|
df.to_excel(ordner / name, sheet_name="Status_Historie", index=False)
|
|
print("pflege: 4 Dateien")
|
|
|
|
|
|
def _archiv(nummer, kunde, debitor, datum, plz_ort, beschreibung="Alt-Projekt"):
|
|
return {
|
|
"Nummer": nummer, "Kunde": kunde, "Debitor": debitor, "Datum": datum,
|
|
"PLZ, Ort": plz_ort, "Beschreibung": beschreibung,
|
|
"Telefon": "0341-9", "Mobil": "", "E-Mail": "alt@example.de",
|
|
"Ansprechpartner": "Herr Alt",
|
|
}
|
|
|
|
|
|
def schreibe_archiv():
|
|
ordner = INPUT / "archiv"
|
|
ordner.mkdir(parents=True, exist_ok=True)
|
|
|
|
df1 = pd.DataFrame([
|
|
_archiv("B-9001", "Altkunde Nord", 80001, "2015-05-01", "10115 Berlin"),
|
|
])
|
|
df2 = pd.DataFrame([
|
|
_archiv("B-9002", "Altkunde Süd", 80002, "2016-06-01", "04109 Leipzig"),
|
|
])
|
|
# Datei 3: A-1002 ist bereits im Export -> wird ignoriert (Export gewinnt);
|
|
# B-9001 ist Dublette aus Datei 1 -> wird entfernt.
|
|
df3 = pd.DataFrame([
|
|
_archiv("A-1002", "Bauer AG ARCHIV", 70002, "2014-01-01", "20095 Hamburg"),
|
|
_archiv("B-9001", "Altkunde Nord", 80001, "2015-05-01", "10115 Berlin"),
|
|
])
|
|
df4 = pd.DataFrame([
|
|
_archiv("B-9003", "Altkunde West", 80003, "2017-07-01", "50667 Köln"),
|
|
])
|
|
|
|
for name, df in [
|
|
("archiv_1.xlsx", df1),
|
|
("archiv_2.xlsx", df2),
|
|
("archiv_3_dup.xlsx", df3),
|
|
("archiv_4.xlsx", df4),
|
|
]:
|
|
df.to_excel(ordner / name, sheet_name="Archiv_Stammdaten", index=False)
|
|
print("archiv: 4 Dateien")
|
|
|
|
|
|
# Detail-Spalten werden vom ETL positionsbasiert umbenannt; Kopfzeile in Zeile 4
|
|
# (header=3). Spaltennamen hier nur informativ — "Postleitzahl" steuert dtype=str.
|
|
DETAIL_COLS = ["Bieter", "Gebot-Nr", "Zuschlags-Nr", "Bundesland",
|
|
"Landkreis", "Postleitzahl", "Gemeinde", "Gemarkung", "Flurstück"]
|
|
|
|
|
|
def _detail(bieter, gebot, zuschlag, bundesland, kreis, plz, gemeinde,
|
|
gemarkung, flurstueck):
|
|
return dict(zip(DETAIL_COLS, [bieter, gebot, zuschlag, bundesland, kreis,
|
|
plz, gemeinde, gemarkung, flurstueck]))
|
|
|
|
|
|
def _schreibe_zuschlag_datei(pfad, detail_rows, kompakt_rows):
|
|
detail = pd.DataFrame(detail_rows, columns=DETAIL_COLS)
|
|
kompakt = pd.DataFrame(kompakt_rows, columns=["Zuschlags-Nr", "Gebotsmenge"])
|
|
with pd.ExcelWriter(pfad, engine="openpyxl") as xls:
|
|
# startrow=3 -> Kopfzeile landet in Zeile 4 (read_excel header=3)
|
|
detail.to_excel(xls, sheet_name="Zuschläge_Detailliert",
|
|
index=False, startrow=3)
|
|
kompakt.to_excel(xls, sheet_name="Zuschläge_Kompakt",
|
|
index=False, startrow=3)
|
|
|
|
|
|
def schreibe_zuschlaege():
|
|
ordner = INPUT / "zuschlaege"
|
|
ordner.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Datei 1: Z-100 mit zwei Flurstücken (beide bleiben erhalten)
|
|
_schreibe_zuschlag_datei(
|
|
ordner / "zuschlaege_1.xlsx",
|
|
[_detail("WindCo", "G-01", "Z-100", "Berlin", "Kreis A", "10115",
|
|
"Berlin", "GemA", "F1"),
|
|
_detail("WindCo", "G-01", "Z-100", "Berlin", "Kreis A", "10115",
|
|
"Berlin", "GemA", "F2")],
|
|
[["Z-100", 5000]],
|
|
)
|
|
# Datei 2: Z-200 in München (PLZ hat eigenes Angebot -> Flag True)
|
|
_schreibe_zuschlag_datei(
|
|
ordner / "zuschlaege_2.xlsx",
|
|
[_detail("SolarCo", "G-02", "Z-200", "Bayern", "Kreis B", "80331",
|
|
"München", "GemB", "F3")],
|
|
[["Z-200", 3000]],
|
|
)
|
|
# Datei 3: exakte Dublette (Z-100, F1) -> wird per Dedup entfernt
|
|
_schreibe_zuschlag_datei(
|
|
ordner / "zuschlaege_3_dup.xlsx",
|
|
[_detail("WindCo", "G-01", "Z-100", "Berlin", "Kreis A", "10115",
|
|
"Berlin", "GemA", "F1")],
|
|
[["Z-100", 5000]],
|
|
)
|
|
# Datei 4: Z-300 mit Leipzig (führende Null!) + PLZ ohne Geo-Treffer (99999)
|
|
_schreibe_zuschlag_datei(
|
|
ordner / "zuschlaege_4.xlsx",
|
|
[_detail("HydroCo", "G-03", "Z-300", "Sachsen", "Kreis C", "04109",
|
|
"Leipzig", "GemC", "F4"),
|
|
_detail("HydroCo", "G-03", "Z-300", "Sachsen", "Kreis C", "99999",
|
|
"Nirgendwo", "GemD", "F5")],
|
|
[["Z-300", 7000]],
|
|
)
|
|
print("zuschlaege: 4 Dateien")
|
|
|
|
|
|
def main():
|
|
if INPUT.exists():
|
|
import shutil
|
|
shutil.rmtree(INPUT)
|
|
INPUT.mkdir(parents=True)
|
|
schreibe_export()
|
|
schreibe_pflege()
|
|
schreibe_archiv()
|
|
schreibe_zuschlaege()
|
|
print(f"\nFixtures erzeugt unter: {INPUT}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|