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
|
||||
„Excel-Import-Mapping"). Erzeugt je Datei einen Objekttyp + Beladungsvorlage
|
||||
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.
|
||||
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"}
|
||||
SPALTE_ARTIKEL = "B"
|
||||
SPALTE_MENGE = "C"
|
||||
SPALTE_ARTIKELNUMMER = "D"
|
||||
SPALTE_ABLAUFHINWEIS = "D"
|
||||
SPALTE_SERIENNUMMER = "E"
|
||||
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
|
||||
class ImportZeile:
|
||||
fach: str | None
|
||||
artikel: str
|
||||
menge: float
|
||||
artikelnummer: str | None
|
||||
hat_ablaufhinweis: bool
|
||||
hat_seriennummer: bool
|
||||
|
||||
|
||||
@@ -75,11 +97,12 @@ def _lade_shared_strings(archiv: zipfile.ZipFile) -> list[str]:
|
||||
|
||||
|
||||
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,
|
||||
D=Artikelnummer, E=Seriennummer (optional). Zeilen mit gesetztem B aber
|
||||
ohne gesetztes C sind Fach-Überschriften, kein Artikel.
|
||||
D=Ablaufdatum (Excel-Datumsserial, oft plausibel, gelegentlich Platzhalter/
|
||||
Freitext), E=Seriennummer (optional). Zeilen mit gesetztem B aber ohne
|
||||
gesetztes C sind Fach-Überschriften, kein Artikel.
|
||||
"""
|
||||
with zipfile.ZipFile(pfad) as archiv:
|
||||
strings = _lade_shared_strings(archiv)
|
||||
@@ -123,7 +146,7 @@ def lies_xlsx(pfad: Path) -> ImportDatei:
|
||||
fach=aktuelles_fach,
|
||||
artikel=artikel,
|
||||
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,
|
||||
)
|
||||
)
|
||||
@@ -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:
|
||||
"""Abgleich primär über Artikelnummer (Prompt-Vorgabe: Namensgleichheit ist
|
||||
Tippfehler-anfällig). Ohne Artikelnummer bleibt Name der einzige Schlüssel
|
||||
für diesen Importlauf - Review-Pflicht laut Sprintplan.
|
||||
"""Abgleich über den Namen - die drei Quelllisten haben keine echte
|
||||
Artikelnummer-Spalte (Review-Fund, siehe Modul-Docstring). Tippfehler-Risiko
|
||||
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:
|
||||
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(
|
||||
name=zeile.artikel,
|
||||
artikelnummer=zeile.artikelnummer,
|
||||
einheit=STANDARD_EINHEIT,
|
||||
materialtyp=materialtyp,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user