fix(import): Spalte D ist Ablaufdatum-Hinweis, keine Artikelnummer
CI / backend-tests (push) Successful in 53s
CI / frontend-build (push) Successful in 53s

Nutzer-Fund: "Kompresse 10x10 / 5 / Apr 29" - Wert 47209 in Spalte D ist
Excel-Datum 2029-04-01, kein Artikelnummer. Vollständige Prüfung aller drei
Listen bestätigt: D ist überwiegend ein echtes Ablaufdatum, keine
Artikelnummer-Spalte existiert überhaupt. Material-Abgleich läuft jetzt über
den Namen; ein plausibles Datum in Spalte D klassifiziert das Material als
'ablauf_charge' statt 'standard' (Wert selbst wird nicht gespeichert - gehört
auf objektposition, nicht auf die Vorlage). Bekannte Ausreißer (Platzhalter-
wert 12754, Freitext "Jahr 2023"/"MTK 23") werden per Plausibilitätsfenster
ignoriert statt fehlzuklassifizieren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
2026-09-04 00:58:25 +02:00
co-authored by Claude Sonnet 5
parent dcc75dd2d0
commit f47cefcc28
2 changed files with 48 additions and 26 deletions
+41 -19
View File
@@ -2,7 +2,7 @@
(Prompt-Sprintplan Abschnitt 5, Detail-Mapping in ergebnisse/testphasen.md (Prompt-Sprintplan Abschnitt 5, Detail-Mapping in ergebnisse/testphasen.md
„Excel-Import-Mapping"). Erzeugt je Datei einen Objekttyp + Beladungsvorlage „Excel-Import-Mapping"). Erzeugt je Datei einen Objekttyp + Beladungsvorlage
Version 1 + Vorlagenpositionen; legt Material bei Bedarf an (Abgleich über Version 1 + Vorlagenpositionen; legt Material bei Bedarf an (Abgleich über
Artikelnummer, nicht über Namensgleichheit). den Namen - die Listen haben keine echte Artikelnummer-Spalte, siehe unten).
Deployment-Regel (Prompt 19): dieses Skript wird hier nur als Code erzeugt. Deployment-Regel (Prompt 19): dieses Skript wird hier nur als Code erzeugt.
Ausführung nur auf dem Zielsystem/Test-Deployment gegen eine echte DB, nicht Ausführung nur auf dem Zielsystem/Test-Deployment gegen eine echte DB, nicht
@@ -39,17 +39,39 @@ from app.models.vorlage import Beladungsvorlage, Vorlagenposition, VorlageStatus
NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"} NS = {"a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
SPALTE_ARTIKEL = "B" SPALTE_ARTIKEL = "B"
SPALTE_MENGE = "C" SPALTE_MENGE = "C"
SPALTE_ARTIKELNUMMER = "D" SPALTE_ABLAUFHINWEIS = "D"
SPALTE_SERIENNUMMER = "E" SPALTE_SERIENNUMMER = "E"
STANDARD_EINHEIT = "Stück" STANDARD_EINHEIT = "Stück"
# Excel-Datumsserial (Tage seit 1899-12-30) - Spalte D ist als Datum formatiert
# und enthält bei den meisten Zeilen ein echtes Ablaufdatum (z. B. 47209 ->
# 2029-04-01, geprüft anhand der Reihe "Kompresse 10x10 / 5 / Apr 29" aus der
# Quelldatei). Ursprüngliche Annahme "D=Artikelnummer" war falsch - es gibt in
# den drei Listen gar keine echte Artikelnummer-Spalte. Plausibilitätsfenster
# grenzt Platzhalter-/Fehlwerte aus (z. B. 12754 -> 1934-12-01, mehrfach mit
# identischem Wert bei völlig unterschiedlichen Artikeln - kein echtes Datum).
_EXCEL_EPOCH_TAGE_MIN = 42000 # ca. 2015-01-01
_EXCEL_EPOCH_TAGE_MAX = 51000 # ca. 2039-08-01
def _ist_plausibles_ablaufdatum(roh_wert: str | None) -> bool:
if roh_wert is None:
return False
try:
tage = int(roh_wert)
except ValueError:
# Freitext wie "Jahr 2023" oder "MTK 23" kommt in den Quelldaten vor -
# kein auswertbares Datum, aber auch kein Grund den Import abzubrechen.
return False
return _EXCEL_EPOCH_TAGE_MIN <= tage <= _EXCEL_EPOCH_TAGE_MAX
@dataclass @dataclass
class ImportZeile: class ImportZeile:
fach: str | None fach: str | None
artikel: str artikel: str
menge: float menge: float
artikelnummer: str | None hat_ablaufhinweis: bool
hat_seriennummer: bool hat_seriennummer: bool
@@ -75,11 +97,12 @@ def _lade_shared_strings(archiv: zipfile.ZipFile) -> list[str]:
def lies_xlsx(pfad: Path) -> ImportDatei: def lies_xlsx(pfad: Path) -> ImportDatei:
"""Liest Fach/Artikel/Menge/Artikelnummer/SN-Hinweis aus dem ersten Arbeitsblatt. """Liest Fach/Artikel/Menge/Ablaufhinweis/SN-Hinweis aus dem ersten Arbeitsblatt.
Layout laut Mapping-Tabelle (ergebnisse/testphasen.md): B=Artikel, C=Menge, Layout laut Mapping-Tabelle (ergebnisse/testphasen.md): B=Artikel, C=Menge,
D=Artikelnummer, E=Seriennummer (optional). Zeilen mit gesetztem B aber D=Ablaufdatum (Excel-Datumsserial, oft plausibel, gelegentlich Platzhalter/
ohne gesetztes C sind Fach-Überschriften, kein Artikel. Freitext), E=Seriennummer (optional). Zeilen mit gesetztem B aber ohne
gesetztes C sind Fach-Überschriften, kein Artikel.
""" """
with zipfile.ZipFile(pfad) as archiv: with zipfile.ZipFile(pfad) as archiv:
strings = _lade_shared_strings(archiv) strings = _lade_shared_strings(archiv)
@@ -123,7 +146,7 @@ def lies_xlsx(pfad: Path) -> ImportDatei:
fach=aktuelles_fach, fach=aktuelles_fach,
artikel=artikel, artikel=artikel,
menge=menge_zahl, menge=menge_zahl,
artikelnummer=werte.get(SPALTE_ARTIKELNUMMER), hat_ablaufhinweis=_ist_plausibles_ablaufdatum(werte.get(SPALTE_ABLAUFHINWEIS)),
hat_seriennummer=werte.get(SPALTE_SERIENNUMMER) is not None, hat_seriennummer=werte.get(SPALTE_SERIENNUMMER) is not None,
) )
) )
@@ -151,24 +174,23 @@ async def _hole_oder_erstelle_kategorie(db: AsyncSession, bereich: Bereich, name
async def _hole_oder_erstelle_material(db: AsyncSession, zeile: ImportZeile) -> Material: async def _hole_oder_erstelle_material(db: AsyncSession, zeile: ImportZeile) -> Material:
"""Abgleich primär über Artikelnummer (Prompt-Vorgabe: Namensgleichheit ist """Abgleich über den Namen - die drei Quelllisten haben keine echte
Tippfehler-anfällig). Ohne Artikelnummer bleibt Name der einzige Schlüssel Artikelnummer-Spalte (Review-Fund, siehe Modul-Docstring). Tippfehler-Risiko
r diesen Importlauf - Review-Pflicht laut Sprintplan. bleibt daher wie im Sprintplan vermerkt eine manuelle Review-Pflicht.
""" """
material = None material = (await db.execute(select(Material).where(Material.name == zeile.artikel))).scalar_one_or_none()
if zeile.artikelnummer:
material = (
await db.execute(select(Material).where(Material.artikelnummer == zeile.artikelnummer))
).scalar_one_or_none()
if material is None:
material = (await db.execute(select(Material).where(Material.name == zeile.artikel))).scalar_one_or_none()
if material is not None: if material is not None:
return material return material
materialtyp = MaterialTyp.geraet_sn if zeile.hat_seriennummer else MaterialTyp.standard if zeile.hat_seriennummer:
materialtyp = MaterialTyp.geraet_sn
elif zeile.hat_ablaufhinweis:
materialtyp = MaterialTyp.ablauf_charge
else:
materialtyp = MaterialTyp.standard
material = Material( material = Material(
name=zeile.artikel, name=zeile.artikel,
artikelnummer=zeile.artikelnummer,
einheit=STANDARD_EINHEIT, einheit=STANDARD_EINHEIT,
materialtyp=materialtyp, materialtyp=materialtyp,
) )
+7 -7
View File
@@ -105,10 +105,10 @@ Grundlage: die drei bestehenden Listen im Repo (`Handball Rucksack.xlsx`, `Rettu
|---|---|---| |---|---|---|
| Datei selbst (z. B. „Handball Rucksack") | `objekttyp.name` + `beladungsvorlage.name` (z. B. „Rucksack Handball Standard") | Ein Import-Lauf pro Datei erzeugt einen Objekttyp + eine Vorlage Version 1 | | Datei selbst (z. B. „Handball Rucksack") | `objekttyp.name` + `beladungsvorlage.name` (z. B. „Rucksack Handball Standard") | Ein Import-Lauf pro Datei erzeugt einen Objekttyp + eine Vorlage Version 1 |
| Fach-Überschriftzeile (z. B. „Fach 1") | `vorlagenposition.fach` | Freitext übernehmen wie in Excel geschrieben, keine Normalisierung nötig | | Fach-Überschriftzeile (z. B. „Fach 1") | `vorlagenposition.fach` | Freitext übernehmen wie in Excel geschrieben, keine Normalisierung nötig |
| Artikel-Spalte | `material.name` (neu anlegen, falls Name noch nicht im Materialstamm existiert) | Abgleich über Artikelnummer, nicht über Namensgleichheit (Tippfehler-Gefahr) | | Artikel-Spalte | `material.name` (neu anlegen, falls Name noch nicht im Materialstamm existiert) | Abgleich über den Namen - die drei Listen haben **keine** echte Artikelnummer-Spalte (Korrektur, siehe unten), Tippfehler-Risiko bleibt manuelle Review-Pflicht |
| Artikelnummer-Spalte | `material.artikelnummer` | Primärer Abgleichsschlüssel beim Import: gleiche Artikelnummer über mehrere Listen hinweg → derselbe Materialstamm-Eintrag, nicht dubliziert anlegen |
| Menge-Spalte | `vorlagenposition.sollmenge` | Numerisch parsen, Fehleinträge (z. B. „ca. 5") vor Import manuell bereinigen | | Menge-Spalte | `vorlagenposition.sollmenge` | Numerisch parsen, Fehleinträge (z. B. „ca. 5") vor Import manuell bereinigen |
| Seriennummer-Spalte (falls vorhanden) | Materialtyp-Erkennung: Vorhandensein einer SN-Spalte bei dieser Zeile → `material.materialtyp = 'geraet_sn'`, sonst `'standard'` | Ablaufdatum/Charge kommt in den bestehenden Listen nicht vor → für diese drei Listen entfällt Typ `ablauf_charge` beim Erstimport, wird später manuell ergänzt (z. B. Infusionssysteme) | | Spalte D (Excel-Zellformat: Datum) | Materialtyp-Erkennung: plausibles Ablaufdatum (Jahr ~2015-2039) in dieser Zeile → `material.materialtyp = 'ablauf_charge'` | Korrektur: ursprünglich fälschlich als Artikelnummer gemappt, siehe „Bekannte Lücken" unten. Datum selbst wird NICHT gespeichert (gehört auf `objektposition`, nicht auf die Vorlage - Ablaufdatum ist pro physischem Exemplar, nicht pro Soll-Position) |
| Seriennummer-Spalte (falls vorhanden) | Materialtyp-Erkennung: Vorhandensein einer SN-Spalte bei dieser Zeile → `material.materialtyp = 'geraet_sn'` (hat Vorrang vor Ablaufdatum-Erkennung) | |
| Einheit | nicht in Excel vorhanden | Beim Import Standardwert „Stück" setzen, danach manuell korrigieren wo nötig (z. B. „Paar", „Packung") | | Einheit | nicht in Excel vorhanden | Beim Import Standardwert „Stück" setzen, danach manuell korrigieren wo nötig (z. B. „Paar", „Packung") |
| Kategorie/Bereich | nicht in Excel vorhanden | Einmalig vor Import anlegen: 1 Bereich „Rettungsdienst/KatS", grobe Kategorie „Rucksäcke" für alle drei Objekttypen | | Kategorie/Bereich | nicht in Excel vorhanden | Einmalig vor Import anlegen: 1 Bereich „Rettungsdienst/KatS", grobe Kategorie „Rucksäcke" für alle drei Objekttypen |
@@ -116,16 +116,16 @@ Grundlage: die drei bestehenden Listen im Repo (`Handball Rucksack.xlsx`, `Rettu
1. Bereich + Kategorie einmalig anlegen (Administration, manuell oder Skript). 1. Bereich + Kategorie einmalig anlegen (Administration, manuell oder Skript).
2. Je Excel-Datei: Objekttyp + Beladungsvorlage Version 1 anlegen. 2. Je Excel-Datei: Objekttyp + Beladungsvorlage Version 1 anlegen.
3. Zeilen einlesen (stdlib `zipfile`/`xml.etree.ElementTree` wie bei der Erstanalyse, siehe Prompt 01 kein `openpyxl` nötig): Fach, Artikel, Artikelnummer, Menge, optionale SN-Spalte. 3. Zeilen einlesen (stdlib `zipfile`/`xml.etree.ElementTree` wie bei der Erstanalyse, siehe Prompt 01 kein `openpyxl` nötig): Fach, Artikel, Menge, Ablaufdatum-Hinweis (Spalte D), optionale SN-Spalte.
4. Je Zeile: Material anhand Artikelnummer suchen oder neu anlegen, dann Vorlagenposition mit Sollmenge und Fach erzeugen. 4. Je Zeile: Material anhand des Namens suchen oder neu anlegen, dann Vorlagenposition mit Sollmenge und Fach erzeugen.
5. Konkrete Objekte (z. B. „Rucksack Handball 12", „Rucksack Handball 17") werden NICHT aus Excel importiert (Excel enthält nur die Soll-Struktur, keine konkreten Instanzen) Administration legt sie danach manuell an, je mit Code (Karte 10) und Vorlagen-Referenz. 5. Konkrete Objekte (z. B. „Rucksack Handball 12", „Rucksack Handball 17") werden NICHT aus Excel importiert (Excel enthält nur die Soll-Struktur, keine konkreten Instanzen) Administration legt sie danach manuell an, je mit Code (Karte 10) und Vorlagen-Referenz.
6. Erste Kontrolle je neu angelegtem Objekt erfasst den tatsächlichen Ist-Bestand (Objekte starten „nicht kontrolliert", analog Duplizieren-Regel Prompt 09) dies IST der Übergang vom Papier- zum Digitalbetrieb (Prompt 23). 6. Erste Kontrolle je neu angelegtem Objekt erfasst den tatsächlichen Ist-Bestand (Objekte starten „nicht kontrolliert", analog Duplizieren-Regel Prompt 09) dies IST der Übergang vom Papier- zum Digitalbetrieb (Prompt 23).
### Bekannte Lücken, vor Import zu klären ### Bekannte Lücken, vor Import zu klären
- Einheiten pro Material sind in den Excel-Listen nicht erfasst vor Import Liste durchgehen und einmalig ergänzen (einmaliger manueller Aufwand, keine Systemfrage). - Einheiten pro Material sind in den Excel-Listen nicht erfasst vor Import Liste durchgehen und einmalig ergänzen (einmaliger manueller Aufwand, keine Systemfrage).
- Uneindeutige/fehlende Artikelnummern in den Quell-Listen (falls vorhanden) verhindern automatischen Abgleich in diesen Fällen manuelle Zuordnung beim Import nötig.
- Import-Skript selbst ist Code (Prompt-19-Deployment-Regel gilt: bei Umsetzung nur Skript-Datei erzeugen, Ausführung gegen Test-DB erst auf Zielsystem/Test-Deployment, nicht lokal). - Import-Skript selbst ist Code (Prompt-19-Deployment-Regel gilt: bei Umsetzung nur Skript-Datei erzeugen, Ausführung gegen Test-DB erst auf Zielsystem/Test-Deployment, nicht lokal).
- Spalte D ist in den xlsx-Rohdaten teils als Datum formatiert (Excel-Zellformat, nicht Inhalt) - Werte wie 12754 sind aber kein plausibles Datum (Jahr ~1934), Mapping D=Artikelnummer bleibt daher wie oben bestätigt. Skript (`backend/scripts/import_excel.py`) liest den Rohwert unabhängig vom Zellformat. - **Korrektur (nach echtem Import-Lauf, vom Nutzer anhand "Kompresse 10x10 / 5 / Apr 29" bemerkt):** Spalte D wurde ursprünglich fälschlich als Artikelnummer gemappt. Vollständige Prüfung aller drei Listen zeigt: D ist bei den meisten Zeilen ein echtes Excel-Datum (z. B. 47209 → 2029-04-01, angezeigt „Apr 29") - ein Ablaufdatum-Hinweis, keine Artikelnummer. Es gibt in den drei Listen **keine** Artikelnummer-Spalte; Material-Abgleich läuft daher über den Namen. Ausnahmen in den Rohdaten: der Wert 12754 (→ 1934-12-01) wiederholt sich identisch bei mehreren unterschiedlichen Artikeln und ist offensichtlich kein echtes Datum, sondern ein Platzhalter/Fehlwert; vereinzelt steht in Spalte D auch Freitext („Jahr 2023", „MTK 23"). Das Importskript (`backend/scripts/import_excel.py`) akzeptiert daher nur Werte in einem plausiblen Jahresfenster (~2015-2039) als Ablaufdatum-Hinweis und nutzt sie ausschließlich zur `materialtyp`-Klassifikation (`ablauf_charge`) - das konkrete Datum selbst wird nicht gespeichert (gehört auf `objektposition`, nicht auf die Vorlage).
- Bereits mit der alten (falschen) Logik importierte Test-Daten auf dem Zielsystem enthalten fehlerhafte `material.artikelnummer`-Werte (die eigentlich Datums-Rohwerte waren) und teils falschen `materialtyp` (`standard` statt `ablauf_charge`) - Bereinigung/Neu-Import auf dem Testsystem nötig, siehe Projekt-Historie.
- Gleiches Material kommt in einer Liste teils in mehreren Fächern vor (z. B. Verbandsmaterial in mehreren Taschen) - da `vorlagenposition` nur eine Zeile pro Material+Vorlage zulässt (Prompt 20-Schema), führt das Importskript solche Duplikate zusammen (Mengen addiert, Fächer als Liste), erste Erkenntnis erst beim echten Import-Lauf gegen den Zielserver, nicht vorher erkannt. - Gleiches Material kommt in einer Liste teils in mehreren Fächern vor (z. B. Verbandsmaterial in mehreren Taschen) - da `vorlagenposition` nur eine Zeile pro Material+Vorlage zulässt (Prompt 20-Schema), führt das Importskript solche Duplikate zusammen (Mengen addiert, Fächer als Liste), erste Erkenntnis erst beim echten Import-Lauf gegen den Zielserver, nicht vorher erkannt.
## Regeln übergreifend ## Regeln übergreifend