; ==============================================================================
; _X31_Export  –  Hauptfunktion
; ==============================================================================
Func _X31_Export($sImportTxt, $sAusgabeX31)

    ; ── 1. Datei einlesen ────────────────────────────────────
    Local $hLese = FileOpen($sImportTxt, 0)
    If $hLese = -1 Then
        MsgBox(16, "Fehler", "Importdatei nicht gefunden:" & @CRLF & $sImportTxt)
        Return SetError(1, 0, False)
    EndIf
    Local $aRawLines[10001]
    $aRawLines[0] = 0
    While True
        Local $sZ = FileReadLine($hLese)
        If @error = -1 Then ExitLoop
        $sZ = StringReplace($sZ, Chr(13), "")
        If $aRawLines[0] < 10000 Then
            $aRawLines[0] += 1
            $aRawLines[$aRawLines[0]] = $sZ
        EndIf
    WEnd
    FileClose($hLese)
    If $aRawLines[0] = 0 Then
        MsgBox(16, "Fehler", "Importdatei ist leer.")
        Return SetError(2, 0, False)
    EndIf

    ; ── 2. Kopfdaten lesen ───────────────────────────────────
    Local $aKopf[30][2]
    Local $iKopfN  = 0
    Local $bInKopf = False
    For $i = 1 To $aRawLines[0]
        Local $sL = StringStripWS($aRawLines[$i], 3)
        If StringInStr($sL, "[Kopfdaten]") Then
            $bInKopf = True
            ContinueLoop
        EndIf
        If StringLeft($sL, 1) = "[" And $bInKopf Then
            $bInKopf = False
            ContinueLoop
        EndIf
        If $bInKopf Then
            Local $iG = StringInStr($sL, "=")
            If $iG > 0 And $iKopfN < 28 Then
                $aKopf[$iKopfN][0] = StringStripWS(StringLeft($sL, $iG - 1), 3)
                $aKopf[$iKopfN][1] = StringStripWS(StringMid($sL, $iG + 1), 3)
                $iKopfN += 1
            EndIf
        EndIf
    Next
    ; Fallback: erste 15 Zeilen
    If $iKopfN = 0 Then
        For $i = 1 To ($aRawLines[0] < 15 ? $aRawLines[0] : 15)
            Local $sF = StringStripWS($aRawLines[$i], 3)
            Local $pF = StringInStr($sF, "=")
            If $pF > 0 And $iKopfN < 28 Then
                $aKopf[$iKopfN][0] = StringStripWS(StringLeft($sF, $pF - 1), 3)
                $aKopf[$iKopfN][1] = StringStripWS(StringMid($sF, $pF + 1), 3)
                $iKopfN += 1
            EndIf
        Next
    EndIf

    Local $sDatum      = _KopfGet($aKopf, "Datum")
    Local $sBaustelle  = _KopfGet($aKopf, "Baustelle")
    Local $sBauabs     = _KopfGet($aKopf, "Bauabschnitt")
    Local $sVertrag    = _KopfGet($aKopf, "Vertrag")
    Local $sAspaN      = _KopfGet($aKopf, "AspaN")
    Local $sAspaTel    = _KopfGet($aKopf, "AspaTel")
    Local $sISODatum   = _DatumISO($sDatum)
    Local $sUID        = _UUID()
    Local $sBoQUID     = _UUID()
    Local $sLVName     = ($sVertrag <> "") ? $sVertrag : ($sBaustelle & " " & $sBauabs)

    ; Feldlängen auf Dataflor-Limits kürzen
    Local $sLVName20   = StringLeft($sLVName,   20)
    Local $sBaust50    = StringLeft($sBaustelle, 50)

    ; ── 3. Aufmassdaten parsen ───────────────────────────────
    ; Spalten (0-basiert nach Split mit |):
    ;   0=Ort, 1=OZ, 2=Faktor, 3=Einzelmenge, 4,5=leer, 6=GesamtMenge,
    ;   7=Einheit, 8=Beschreibung, 9=Bemerkung, 10=Menge2, 11=EP, 12=GP
    Local $aPosData[10001][9]
    ; [n][0]=OZ, [n][1]=Qty(float), [n][2]=Ort, [n][3]=Beschreibung
    ; [n][4]=Bemerkung, [n][5]=Einheit, [n][6]=EP, [n][7]=Faktor, [n][8]=EinzelMenge
    Local $iPosN   = 0
    Local $bInData = False

    For $i = 1 To $aRawLines[0]
        Local $sLine = StringStripWS($aRawLines[$i], 3)
        If StringInStr($sLine, "[Aufma") Then
            $bInData = True
            ContinueLoop
        EndIf
        If StringLeft($sLine, 1) = "[" And $bInData Then
            $bInData = False
            ContinueLoop
        EndIf
        If Not $bInData Then ContinueLoop
        If $sLine = "" Then ContinueLoop
        If StringStripWS(StringReplace($sLine, "|", ""), 3) = "" Then ContinueLoop
        If Not StringInStr($sLine, "|") Then ContinueLoop

        Local $aS = StringSplit($sLine, "|", $STR_NOCOUNT)
        Local $nS = UBound($aS)
        If $nS < 2 Then ContinueLoop

        Local $sOZ = ($nS > 1) ? StringStripWS($aS[1], 3) : ""
        If Not _IsValidOZ($sOZ) Then ContinueLoop

        Local $sOrt     = ($nS > 0) ? StringStripWS($aS[0], 3) : ""
        Local $sFaktor  = ($nS > 2) ? StringStripWS($aS[2], 3) : "1"
        Local $sEinzel  = ($nS > 3) ? StringStripWS($aS[3], 3) : ""
        Local $sGesamt  = ($nS > 10) ? StringStripWS($aS[10], 3) : ""
        Local $sEinh    = ($nS > 7) ? StringStripWS($aS[7], 3) : ""
        Local $sBeschr  = ($nS > 8) ? StringStripWS($aS[8], 3) : ""
        Local $sBemerk  = ($nS > 9) ? StringStripWS($aS[9], 3) : ""
        Local $sEP      = ($nS > 11) ? StringStripWS($aS[11], 3) : ""

        ; Qty: Spalte 6 (Gesamtmenge) hat immer Vorrang – sie ist bereits mit Faktor berechnet.
        ; Faktor * Einzelmenge wird NICHT selbst gerechnet, da Spalte 6 das Ergebnis enthält.
        ; Nur wenn Spalte 6 leer/0: Einzelmenge als Fallback, dann Faktor.
        Local $fFaktor = _ToFloat($sFaktor)
        Local $fEinzel = _ToFloat($sEinzel)
        Local $fGesamt = _ToFloat($sGesamt)
        Local $fQty    = $fGesamt
;~         If $fGesamt <> 0 Then
;~             $fQty = $fGesamt
;~         ElseIf $fEinzel <> 0 Then
;~             $fQty = $fEinzel
;~         Else
;~             $fQty = $fFaktor
;~         EndIf

        $aPosData[$iPosN][0] = $sOZ
        $aPosData[$iPosN][1] = $fQty
        $aPosData[$iPosN][2] = $sOrt
        $aPosData[$iPosN][3] = $sBeschr
        $aPosData[$iPosN][4] = $sBemerk
        $aPosData[$iPosN][5] = $sEinh
        $aPosData[$iPosN][6] = $sEP
        $aPosData[$iPosN][7] = $sFaktor
        $aPosData[$iPosN][8] = $sEinzel
        $iPosN += 1
        If $iPosN >= 10000 Then ExitLoop
    Next

    If $iPosN = 0 Then
        MsgBox(16, "Fehler", "Keine gueltigen Positionen gefunden.")
        Return SetError(3, 0, False)
    EndIf

    ; ── 4. OZ-Schema ermitteln ───────────────────────────────
    ; KORREKTUR: Dataflor-kompatible Lengths aus dem echten Dataflor-Export
    ; Für Typ B (X.X.XX.XXXX):
    ;   Titel(E1):     Length=1  → '1', '2', '3', ...
    ;   Bauteil(E2):   Length=1  → '2', '3', '4', ... (NICHT 2!)
    ;   Abschnitt(E3): Length=2  → '01', '02', '03', ... (NICHT 3!)
    ;   Position:      Length=4  → '0470', '0010', ...
    ; Für Typ A (XX.XX.XXXX):
    ;   Titel(E1):     Length=2
    ;   Bauteil(E2):   Length=2
    ;   Position:      Length=4
    Local $sOZTyp  = "A"
    Local $iMaxPos = 4

    For $i = 0 To $iPosN - 1
        Local $aOZ = _OZInfo($aPosData[$i][0])
        If $aOZ[0] = "B" Then $sOZTyp = "B"
        If $aOZ[0] = "C" And $sOZTyp = "A" Then $sOZTyp = "C"
        If $aOZ[9] > $iMaxPos Then $iMaxPos = $aOZ[9]
    Next

    ; ── 5. OZ-Hierarchie aufbauen ────────────────────────────
    ; (wie v5, unverändert)

    ; ── 6. Zeit ──────────────────────────────────────────────
    Local $sTime = StringRight("0" & @HOUR, 2) & ":" & StringRight("0" & @MIN, 2) & ":" & StringRight("0" & @SEC, 2)

    ; ── 7. XML aufbauen ──────────────────────────────────────
    Local $sX = ""

    $sX &= '<?xml version="1.0" encoding="UTF-8"?>' & @CRLF
    $sX &= '<!-- REB 23.003 (2009) - X31 Export AutoIt v8 (Dataflor-kompatibel) -->' & @CRLF
    $sX &= '<GAEB xmlns="http://www.gaeb.de/GAEB_DA_XML/DA31/3.3" xmlns:BVBS="BVBS">'

    ; GAEBInfo (kompakt, kein CRLF zwischen Tags - wie Dataflor-Referenz)
    $sX &= '<GAEBInfo>'
    $sX &= '<Version>3.3</Version>'
    $sX &= '<VersDate>2023-01</VersDate>'
    $sX &= '<Date>' & $sISODatum & '</Date>'
    $sX &= '<Time>' & $sTime & '</Time>'
    $sX &= '<ProgSystem>AutoIt REB Engine V1.2</ProgSystem>'
    $sX &= '<ProgName>AutoIt REB X31 Export</ProgName>'
    $sX &= '</GAEBInfo>'

    ; QtyDeterm
    $sX &= '<QtyDeterm>'

    ; PrjInfo
    $sX &= '<PrjInfo>'
    $sX &= '<RefPrjName>' & _XE($sBaust50)  & '</RefPrjName>'
    $sX &= '<RefPrjID>'   & _XE($sLVName20) & '</RefPrjID>'
    $sX &= '</PrjInfo>'

    ; QtyDetermInfo
    $sX &= '<QtyDetermInfo ID="' & $sUID & '">'
    $sX &= '<MethodDescription>REB23003-2009</MethodDescription>'
    $sX &= '</QtyDetermInfo>'

    ; DP
    $sX &= '<DP>31</DP>'

    ; OWN
    $sX &= '<OWN><Address>'
    $sX &= '<Name1>' & _XE($sAspaN) & '</Name1>'
    $sX &= '<Name2></Name2><Name3/><Name4/>'
    $sX &= '<Street></Street><PCode></PCode><City></City>'
    $sX &= '<Contact/><Phone>' & _XE($sAspaTel) & '</Phone><Fax/><Email/>'
    $sX &= '</Address></OWN>'

    ; CTR
    $sX &= '<CTR><Address><Name1/><Name2></Name2><Name3/><Name4/>'
    $sX &= '<Street></Street><PCode></PCode><City></City>'
    $sX &= '<Contact/><Phone/><Fax/><Email/></Address></CTR>'

    ; BoQ
    $sX &= '<BoQ ID="DF_' & _NextID() & '">'
    $sX &= '<RefBoQName>' & _XE($sLVName20) & '</RefBoQName>'
    $sX &= '<RefBoQID>'   & $sBoQUID        & '</RefBoQID>'

    ; ─────────────────────────────────────────────────────────────────────
    ; BoQBkdn – KORRIGIERT für Dataflor-Kompatibilität
    ; Dataflor liest die OZ-Segmentlängen exakt aus diesen Length-Feldern.
    ; Falsche Werte → Import wird lautlos abgebrochen!
    ;
    ; Typ B (X.X.XX.XXXX) → Dataflor-Referenz-Schema:
    ;   Titel     Length=1  (E1: 1 Stelle)
    ;   Bauteil   Length=1  (E2: 1 Stelle, z.B. '3')
    ;   Abschnitt Length=2  (E3: 2 Stellen, z.B. '01')
    ;   Position  Length=4  (Pos: 4 Stellen, z.B. '0470')
    ;   Index     Length=1  (immer)
    ;
    ; Typ A (XX.XX.XXXX):
    ;   Titel     Length=2  (E1)
    ;   Bauteil   Length=2  (E2)
    ;   Position  Length=4
    ;   Index     Length=1
    ; ─────────────────────────────────────────────────────────────────────
    Select
        Case $sOZTyp = "A"
            $sX &= '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Titel</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Bauteil</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>Item</Type><LblBoQBkdn>Position</LblBoQBkdn><Length>' & $iMaxPos & '</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>Index</Type><LblBoQBkdn>Index</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
        Case $sOZTyp = "B"
            ; Dataflor-Referenz (verifiziert): Titel L=1, Bauteil L=1, Abschnitt L=2
            ; Labels exakt wie Dataflor-Export: 'Titel', 'Bauteil', 'Abschnitt'
            $sX &= '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Titel</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Bauteil</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Abschnitt</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>Item</Type><LblBoQBkdn>Position</LblBoQBkdn><Length>' & $iMaxPos & '</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>Index</Type><LblBoQBkdn>Index</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
        Case $sOZTyp = "C"
            $sX &= '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Titel</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>Item</Type><LblBoQBkdn>Position</LblBoQBkdn><Length>' & $iMaxPos & '</Length><Num>No</Num></BoQBkdn>'
            $sX &= '<BoQBkdn><Type>Index</Type><LblBoQBkdn>Index</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
    EndSelect

    ; Ctlg
    $sX &= '<Ctlg><CtlgID>idDIN276_1993</CtlgID>'
    $sX &= '<CtlgType>cost group DIN 276-93</CtlgType>'
    $sX &= '<CtlgName>DIN 276-93</CtlgName></Ctlg>'

    ; BoQBody
    $sX &= '<BoQBody>'

    ; OZ-Zähler für QTakeoff-Blattadressen
    Local $iOZCnt = 0

    ; Eindeutige OZ in TXT-Reihenfolge sammeln
    Local $aOZOrder[10000][4]
    Local $iOZOrderN = 0

    For $i = 0 To $iPosN - 1
        Local $aOZi = _OZInfo($aPosData[$i][0])
        Local $bFound = False
        For $j = 0 To $iOZOrderN - 1
            If $aOZOrder[$j][0] = $aOZi[1] And $aOZOrder[$j][1] = $aOZi[2] And _
               $aOZOrder[$j][2] = $aOZi[3] And $aOZOrder[$j][3] = $aOZi[4] Then
                $bFound = True
                ExitLoop
            EndIf
        Next
        If Not $bFound Then
            $aOZOrder[$iOZOrderN][0] = $aOZi[1]
            $aOZOrder[$iOZOrderN][1] = $aOZi[2]
            $aOZOrder[$iOZOrderN][2] = $aOZi[3]
            $aOZOrder[$iOZOrderN][3] = $aOZi[4]
            $iOZOrderN += 1
        EndIf
    Next

    ; Items ausgeben – hierarchische Struktur
    Local $sCurE1 = ""
    Local $sCurE2 = ""
    Local $sCurE3 = ""
    Local $bIL    = False

    For $oi = 0 To $iOZOrderN - 1
        Local $sE1  = $aOZOrder[$oi][0]
        Local $sE2  = $aOZOrder[$oi][1]
        Local $sE3  = $aOZOrder[$oi][2]
        Local $sPos = $aOZOrder[$oi][3]

        Local $bSameE1 = ($sE1 = $sCurE1)
        Local $bSameE2 = ($sE2 = $sCurE2) And $bSameE1
        Local $bSameE3 = ($sE3 = $sCurE3) And $bSameE2

        ; Schließen bei Wechsel
        If $sOZTyp = "B" And Not $bSameE3 And $bIL Then
            $sX &= '</Itemlist></BoQBody>'
            $sX &= '</BoQCtgy>'
            $bIL = False
            $sCurE3 = ""
        EndIf
        If Not $bSameE2 And $bIL Then
            $sX &= '</Itemlist></BoQBody>'
            $sX &= '</BoQCtgy>'
            $bIL = False
            $sCurE3 = ""
        EndIf
        If $sOZTyp = "B" And Not $bSameE2 And $sCurE2 <> "" Then
            $sX &= '</BoQBody>'
            $sX &= '</BoQCtgy>'
            $sCurE2 = ""
            $sCurE3 = ""
        EndIf
        If Not $bSameE1 And $sCurE1 <> "" Then
            $sX &= '</BoQBody>'
            $sX &= '</BoQCtgy>'
            $sCurE1 = ""
        EndIf

        ; Öffnen
        If $sE1 <> $sCurE1 Then
            $sX &= '<BoQCtgy RNoPart="' & _XE($sE1) & '" ID="DF_' & _NextID() & '">'
            $sX &= '<BoQBody>'
            $sCurE1 = $sE1
        EndIf

        If $sOZTyp = "B" And $sE2 <> $sCurE2 Then
            $sX &= '<BoQCtgy RNoPart="' & _XE($sE2) & '" ID="DF_' & _NextID() & '">'
            $sX &= '<BoQBody>'
            $sCurE2 = $sE2
            $sCurE3 = ""
        EndIf

        If $sOZTyp <> "B" Then $sCurE2 = $sE2
        If $sOZTyp = "B" Then $sCurE3 = $sE3

        If Not $bIL Then
            If $sOZTyp = "B" Then
                $sX &= '<BoQCtgy RNoPart="' & _XE($sCurE3) & '" ID="DF_' & _NextID() & '">'
            Else
                $sX &= '<BoQCtgy RNoPart="' & _XE($sCurE2) & '" ID="DF_' & _NextID() & '">'
            EndIf
            $sX &= '<BoQBody><Itemlist>'
            $bIL = True
        EndIf

        ; Gesamtmenge dieser Position summieren
        Local $fQtySum = 0
        For $i = 0 To $iPosN - 1
            Local $aOZck = _OZInfo($aPosData[$i][0])
            If $aOZck[1] <> $sE1 Or $aOZck[2] <> $sE2 Then ContinueLoop
            If $sOZTyp = "B" And $aOZck[3] <> $sE3 Then ContinueLoop
            If $aOZck[4] <> $sPos Then ContinueLoop
            $fQtySum += $aPosData[$i][1]
        Next

        ; Item schreiben
        $sX &= '<Item ID="DF_' & _NextID() & '" RNoPart="' & _XE($sPos) & '">'
        $sX &= '<QtyDeterm>'
        ; Qty mit Punkt als Dezimaltrenner (XML-Standard)
        $sX &= '<Qty>' & StringReplace(_FmtQty($fQtySum), ",", ".") & '</Qty>'

        ; Alle Ansätze dieser Position ausgeben
        For $i = 0 To $iPosN - 1
            Local $aOZan = _OZInfo($aPosData[$i][0])
            If $aOZan[1] <> $sE1 Or $aOZan[2] <> $sE2 Then ContinueLoop
            If $sOZTyp = "B" And $aOZan[3] <> $sE3 Then ContinueLoop
            If $aOZan[4] <> $sPos Then ContinueLoop
            ; KORREKTUR: _MakeQDetermPair mit BVBS:Explanation
            $sX &= _MakeQDetermPair($aPosData[$i][2], $aPosData[$i][1], $iOZCnt)
        Next

        $sX &= '</QtyDeterm>'
        $sX &= '</Item>'
    Next

    ; Alle offenen Tags schließen
    If $bIL Then
        $sX &= '</Itemlist></BoQBody>'
        $sX &= '</BoQCtgy>'
    EndIf
    If $sOZTyp = "B" And $sCurE2 <> "" Then
        $sX &= '</BoQBody>'
        $sX &= '</BoQCtgy>'
    EndIf
    If $sCurE1 <> "" Then
        $sX &= '</BoQBody>'
        $sX &= '</BoQCtgy>'
    EndIf
    $sX &= '</BoQBody>'
    $sX &= '</BoQ>'
    $sX &= '</QtyDeterm>'
    $sX &= '</GAEB>'

    ; ── 8. Als echtes UTF-8 mit BOM speichern ────────────────
    If FileExists($sAusgabeX31) Then FileDelete($sAusgabeX31)

    Local $binBOM  = Binary("0xEFBBBF")
    Local $binBody = StringToBinary($sX, 4)
    Local $binFull = $binBOM & $binBody

    Local $bOK = FileWrite($sAusgabeX31, $binFull)

    If Not $bOK Or Not FileExists($sAusgabeX31) Then
        MsgBox(16, "Fehler", "Ausgabedatei konnte nicht geschrieben werden:" & @CRLF & $sAusgabeX31)
        Return SetError(4, 0, False)
    EndIf

;~     MsgBox(64, "X31 Export", "X31-Datei erfolgreich erstellt!" & @CRLF & @CRLF & _
;~         "Datei:      " & $sAusgabeX31 & @CRLF & _
;~         "Positionen: " & $iPosN & @CRLF & _
;~         "OZ-Typ:     " & $sOZTyp & @CRLF & _
;~         "Format:     GAEB DA XML 3.3 / DA31 / REB 23.003 - Dataflor-kompatibel")
    Return True
EndFunc


; ==============================================================================
; _MakeQDetermPair($sOrt, $fQty, $iOZCnt)
; Erzeugt EIN K+L-Zeilenpaar als zwei QDetermItem-Blöcke
;
; REIHENFOLGE: K-Zeile (Ort) zuerst, dann L-Zeile (Menge)
;   QDetermItem { QTakeoff Row="*Ort..."
;                 BVBS:Explanation "Ort" }    ← K-Zeile zuerst
;   QDetermItem { QTakeoff Row="Menge..."  }  ← L-Zeile danach
; ==============================================================================
Func _MakeQDetermPair($sOrt, $fQty, ByRef $iOZCnt)
    Local $sQty = _FmtQty($fQty)
    Local $sOZ1 = _REBOZCode($iOZCnt)   ; OZ1 = K-Zeile
    $iOZCnt += 1
    Local $sOZ2 = _REBOZCode($iOZCnt)   ; OZ2 = L-Zeile
    $iOZCnt += 1

    Local $sOut = ""

    ; 1. K-Zeile (Ortsbezeichnung) + BVBS:Explanation – ZUERST
    $sOut &= '<QDetermItem>'
    $sOut &= '<QTakeoff Row="' & _XE(_KZeile($sOrt, $sOZ1)) & '"/>'
    $sOut &= '<BVBS:Explanation>' & _XE(StringStripWS($sOrt, 3)) & '</BVBS:Explanation>'
    $sOut &= '</QDetermItem>'

    ; 2. L-Zeile (Mengenzeile Formel 91) – DANACH
    $sOut &= '<QDetermItem>'
    $sOut &= '<QTakeoff Row="' & _XE(_LZeile($sQty, $sOZ2)) & '"/>'
    $sOut &= '</QDetermItem>'

    Return $sOut
EndFunc


; ==============================================================================
; Hilfsfunktionen
; ==============================================================================

; K-Zeile: 12 Leerzeichen + '*' + Ort(56 Zeichen padded) + OZCode(6) + 5 Leerzeichen = 80 Zeichen
Func _KZeile($sOrt, $sOZCode)
    Local $sOrtPad = StringLeft($sOrt & "                                                        ", 56)
    Return "            *" & $sOrtPad & $sOZCode & "     "
EndFunc

; L-Zeile: 25 Leerzeichen + '100091' + Menge + '=' + (Padding auf 44) + OZCode(6) + 5 Leerzeichen = 80 Zeichen
; Menge mit Komma (DA11-Standard), kein Faktor-Parameter (Menge ist bereits die Gesamtmenge)
Func _LZeile($sMenge, $sOZCode)
    Local $sMengeK = StringReplace(StringStripWS($sMenge, 3), ".", ",")
    Local $sFormel = "100091" & $sMengeK & "="
    While StringLen($sFormel) < 44
        $sFormel &= " "
    WEnd
    $sFormel = StringLeft($sFormel, 44)
    Return "                         " & $sFormel & $sOZCode & "     "
EndFunc

; REB Blattadresse aus Zähler (z.B. 0→"1000A0", 25→"1000Z0", 26→"1001A0")
Func _REBOZCode($iIdx)
    Local $iBlatt  = 1000 + Int($iIdx / 26)
    Local $iZeile  = Mod($iIdx, 26)
    Local $sZeile  = Chr(65 + $iZeile)
    Return String($iBlatt) & $sZeile & "0"
EndFunc

; Qty formatieren: Komma als Dezimaltrenner, 3 Dezimalstellen (trailing zeros entfernt)
Func _FmtQty($fVal)
    Local $fAbs = $fVal
    If $fAbs < 0 Then $fAbs = -$fAbs
    Local $iGanz = Int($fAbs)
    Local $fDez  = $fAbs - $iGanz
    Local $iD3   = Int($fDez * 1000 + 0.5)
    If $iD3 >= 1000 Then
        $iGanz += 1
        $iD3 = 0
    EndIf
    Local $sVorz = ""
    If $fVal < 0 Then $sVorz = "-"
    If $iD3 = 0 Then
        Return $sVorz & String($iGanz)
    EndIf
    Local $sDez = String($iD3)
    While StringLen($sDez) < 3
        $sDez = "0" & $sDez
    WEnd
    While StringRight($sDez, 1) = "0" And StringLen($sDez) > 1
        $sDez = StringLeft($sDez, StringLen($sDez) - 1)
    WEnd
    Return $sVorz & String($iGanz) & "," & $sDez
EndFunc

; OZ analysieren: Gibt Array zurück
; [0]=Typ(A/B/C), [1]=E1, [2]=E2, [3]=E3, [4]=Pos, [5]=OZOriginal
Func _OZInfo($sOZ)
    Local $aI[10]
    $aI[0] = "?"
    $aI[1] = ""
    $aI[2] = ""
    $aI[3] = ""
    $aI[4] = $sOZ
    $aI[5] = $sOZ
    $aI[6] = 0
    $aI[7] = 0
    $aI[8] = 0
    $aI[9] = 0
    If $sOZ = "" Then Return $aI
    If StringRegExp($sOZ, "^\d{6,10}$") Then
        $aI[0] = "C"
        $aI[4] = $sOZ
        $aI[9] = StringLen($sOZ)
        Return $aI
    EndIf
    Local $aS = StringSplit($sOZ, ".", $STR_NOCOUNT)
    Local $n  = UBound($aS)
    If $n = 3 Then
        $aI[0] = "A"
        $aI[1] = $aS[0]
        $aI[6] = StringLen($aS[0])
        $aI[2] = $aS[1]
        $aI[7] = StringLen($aS[1])
        $aI[4] = $aS[2]
        $aI[9] = StringLen($aS[2])
    ElseIf $n = 4 Then
        $aI[0] = "B"
        $aI[1] = $aS[0]
        $aI[6] = StringLen($aS[0])
        $aI[2] = $aS[1]
        $aI[7] = StringLen($aS[1])
        $aI[3] = $aS[2]
        $aI[8] = StringLen($aS[2])
        $aI[4] = $aS[3]
        $aI[9] = StringLen($aS[3])
    EndIf
    $aI[5] = $sOZ
    Return $aI
EndFunc

; OZ-Typ prüfen
Func _IsValidOZ($sOZ)
    If $sOZ = "" Then Return False
    If StringRegExp($sOZ, "^\d{1,4}\.\d{1,4}\.\d{1,6}$") Then Return True
    If StringRegExp($sOZ, "^\d{1,4}\.\d{1,4}\.\d{1,4}\.\d{1,6}$") Then Return True
    If StringRegExp($sOZ, "^\d{6,10}$") Then Return True
    Return False
EndFunc

; Kopfdaten-Wert lesen
Func _KopfGet(ByRef $aKopf, $sKey)
    For $i = 0 To UBound($aKopf) - 1
        If $aKopf[$i][0] = $sKey Then Return $aKopf[$i][1]
    Next
    Return ""
EndFunc

; XML-Sonderzeichen kodieren
Func _XE($s)
    $s = StringReplace($s, "&",  "&amp;")
    $s = StringReplace($s, "<",  "&lt;")
    $s = StringReplace($s, ">",  "&gt;")
    $s = StringReplace($s, '"',  "&quot;")
    $s = StringReplace($s, "'",  "&apos;")
    Return $s
EndFunc

; String zu Float (Komma→Punkt)
Func _ToFloat($s)
    If $s = "" Then Return 0
    $s = StringStripWS($s, 3)
    While StringRight($s, 1) = "," Or StringRight($s, 1) = "."
        $s = StringLeft($s, StringLen($s) - 1)
    WEnd
    $s = StringReplace($s, ",", ".")
    Return Number($s)
EndFunc

; Datum DD.MM.YYYY → YYYY-MM-DD
Func _DatumISO($s)
    $s = StringStripWS($s, 3)
    If StringRegExp($s, "^\d{2}\.\d{2}\.\d{4}$") Then
        Local $aD = StringSplit($s, ".", $STR_NOCOUNT)
        Return $aD[2] & "-" & $aD[1] & "-" & $aD[0]
    EndIf
    Return $s
EndFunc

; Fortlaufende DF_-ID
Func _NextID()
    $g_iIDCnt += 1
    Return $g_iIDCnt
EndFunc

; Pseudo-UUID
Func _UUID()
    Local $sHex = "0123456789abcdef"
    Local $sUID = ""
    Local $aLen[5]
    $aLen[0] = 8
    $aLen[1] = 4
    $aLen[2] = 4
    $aLen[3] = 4
    $aLen[4] = 12
    For $i = 0 To 4
        If $i > 0 Then $sUID &= "-"
        For $j = 1 To $aLen[$i]
            $sUID &= StringMid($sHex, Random(1, 16, 1), 1)
        Next
    Next
    Return $sUID
EndFunc
