fix(import): Spalte D ist Ablaufdatum-Hinweis, keine Artikelnummer
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:
@@ -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
|
||||||
für diesen Importlauf - Review-Pflicht laut Sprintplan.
|
bleibt daher wie im Sprintplan vermerkt eine manuelle Review-Pflicht.
|
||||||
"""
|
"""
|
||||||
material = 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()
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user