Imports ClosedXML.Excel Imports System.Globalization Imports System.IO Imports System.Linq Public Class cVERAG_CustomsDeclarations_Convert Public Const EXPECTED_TEMPLATE_VERSION As String = "CBAM-2026-V1.0" Public Shared Function LOAD_FROM_CBAM_DETAIL_EXCEL(filePath As String, Optional ByRef errors As List(Of String) = Nothing, Optional stopOnFirstInvalidRow As Boolean = False) As List(Of cVERAG_CustomsDeclarations) Dim result As New List(Of cVERAG_CustomsDeclarations) Try If errors Is Nothing Then errors = New List(Of String) If String.IsNullOrWhiteSpace(filePath) Then errors.Add("Es wurde kein Dateipfad übergeben.") Return result End If If Not File.Exists(filePath) Then errors.Add("Datei nicht gefunden: " & filePath) Return result End If Using wb As New XLWorkbook(filePath) Dim ws As IXLWorksheet = Nothing If wb.Worksheets.Any(Function(x) x.Name.Trim().ToUpper() = "CBAM_REPORT") Then ws = wb.Worksheet("CBAM_Report") Else ws = wb.Worksheet(1) End If If ws Is Nothing Then errors.Add("Kein Worksheet gefunden.") Return result End If ' ========================================================= ' Template-Version prüfen ' ========================================================= Dim versionInFile As String = TrimSafe(ws.Cell("F3").GetString()) If Not String.Equals(versionInFile, EXPECTED_TEMPLATE_VERSION, StringComparison.OrdinalIgnoreCase) Then errors.Add("Ungültige Template-Version in F3. Erwartet: '" & EXPECTED_TEMPLATE_VERSION & "', gefunden: '" & versionInFile & "'") Return result End If Dim headerRow As Integer = 13 Dim dataStartRow As Integer = 14 Dim lastRow = If(ws.LastRowUsed() Is Nothing, 0, ws.LastRowUsed().RowNumber()) Dim lastCol = If(ws.LastColumnUsed() Is Nothing, 0, ws.LastColumnUsed().ColumnNumber()) If lastRow < dataStartRow OrElse lastCol = 0 Then errors.Add("Keine Positionsdaten gefunden.") Return result End If ' ========================================================= ' Header-Mapping aufbauen ' ========================================================= Dim colMap As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase) For c As Integer = 1 To lastCol Dim h = TrimSafe(ws.Cell(headerRow, c).GetString()) If h <> "" AndAlso Not colMap.ContainsKey(h) Then colMap.Add(h, c) End If Next ' ========================================================= ' Pflichtspalten im Excel prüfen ' ========================================================= Dim requiredHeaders = New String() {"MRN", "declaration_date", "tariff_code"} For Each req In requiredHeaders If Not colMap.ContainsKey(req) Then errors.Add("Pflichtspalte im Excel nicht gefunden: " & req) End If Next If errors.Any(Function(x) x.StartsWith("Pflichtspalte im Excel nicht gefunden:")) Then Return result End If ' ========================================================= ' Bereits vorhandene / ungültige MRN verwalten ' ========================================================= Dim skippedExistingMrns As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) Dim invalidMrns As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) Dim dict As New Dictionary(Of String, cVERAG_CustomsDeclarations)(StringComparer.OrdinalIgnoreCase) Dim artificialCounter As Integer = 0 For r As Integer = dataStartRow To lastRow Dim mrn As String = GetCellString(ws, r, colMap, "MRN") Dim referenceNo As String = GetCellString(ws, r, colMap, "reference_no") Dim tariffCode As String = GetCellString(ws, r, colMap, "tariff_code") Dim itemNoTxt As String = GetCellString(ws, r, colMap, "position_no") ' Komplette Leerzeile überspringen If mrn = "" AndAlso referenceNo = "" AndAlso tariffCode = "" AndAlso itemNoTxt = "" Then Continue For End If ' ===================================================== ' Zeilenvalidierung ' ===================================================== Dim validationMessage As String = "" If Not ValidateImportRow(ws, r, colMap, validationMessage) Then errors.Add(validationMessage) If mrn <> "" Then invalidMrns.Add(mrn) End If If stopOnFirstInvalidRow Then Return result End If Continue For End If ' Wenn MRN bereits als ungültig markiert wurde -> überspringen If mrn <> "" AndAlso invalidMrns.Contains(mrn) Then Continue For End If ' ===================================================== ' MRN bereits in DB? ' Dann komplette Anmeldung überspringen ' ===================================================== If mrn <> "" Then If skippedExistingMrns.Contains(mrn) Then Continue For End If If MRN_EXISTS_IN_DB(mrn) Then skippedExistingMrns.Add(mrn) errors.Add("MRN bereits vorhanden - Import übersprungen: " & mrn & " (Zeile " & r & ")") Continue For End If End If ' ===================================================== ' Gruppenschlüssel ' ===================================================== Dim grpKey As String = "" If mrn <> "" Then grpKey = "MRN|" & mrn ElseIf referenceNo <> "" Then grpKey = "REF|" & referenceNo Else artificialCounter += 1 grpKey = "AUTO|" & artificialCounter.ToString() End If Dim za As cVERAG_CustomsDeclarations = Nothing If Not dict.ContainsKey(grpKey) Then za = New cVERAG_CustomsDeclarations() za.za_MRN = mrn za.za_LRN = referenceNo za.za_ReferenceCustomer = GetCellString(ws, r, colMap, "customer_reference") za.za_RepresentationCode = GetCellString(ws, r, colMap, "representation_type") za.za_DeclarationDate = GetCellDateNullable(ws, r, colMap, "declaration_date") za.za_ReleaseDate = GetCellDateNullable(ws, r, colMap, "declaration_date") za.za_CountryDispatch = GetCellString(ws, r, colMap, "dispatch_country") za.za_CountryDestination = GetCellString(ws, r, colMap, "destination_country") za.za_CountryImport = GetCellString(ws, r, colMap, "destination_country") za.za_MainProcedure = GetCellString(ws, r, colMap, "requested_procedure") za.za_InvoiceCurrency = GetCellString(ws, r, colMap, "invoice_currency") za.za_InvoiceAmount = GetCellDecimalNullable(ws, r, colMap, "invoice_amount") za.za_IsFinalDeclaration = True za.za_REGIME = "IMPORT" za.za_System = "CBAM_EXCEL_IMPORT" za.za_CustomsSystem = "" za.za_CustomsSystemCountry = "" za.za_IsExternalSystem = True za.za_Firma = "" za.za_Niederlassung = "" za.za_AdditionalProcedure = "" za.za_WarehouseCode = "" za.za_Incoterms = "" za.za_IncotermsPlace = "" za.za_TotGrossMass = Nothing AddOrMergeParty(za.Parties, CreateParty( role:="EXPORTER", eori:=GetCellString(ws, r, colMap, "sender_eori"), name:=GetCellString(ws, r, colMap, "sender_name"), street:=GetCellString(ws, r, colMap, "sender_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "sender_country"), email:="", phone:="" )) AddOrMergeParty(za.Parties, CreateParty( role:="CONSIGNEE", eori:=GetCellString(ws, r, colMap, "recipient_eori"), name:=GetCellString(ws, r, colMap, "recipient_name"), street:=GetCellString(ws, r, colMap, "recipient_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "recipient_country"), email:="", phone:="" )) AddOrMergeParty(za.Parties, CreateParty( role:="IMPORTER", eori:=GetCellString(ws, r, colMap, "importer_eori"), name:=GetCellString(ws, r, colMap, "importer_name"), street:=GetCellString(ws, r, colMap, "importer_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "importer_country"), email:=GetCellString(ws, r, colMap, "importer_email"), phone:=GetCellString(ws, r, colMap, "importer_phone") )) AddOrMergeParty(za.Parties, CreateParty( role:="DECLARANT", eori:=GetCellString(ws, r, colMap, "declarant_eori"), name:=GetCellString(ws, r, colMap, "declarant_name"), street:=GetCellString(ws, r, colMap, "declarant_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "declarant_country"), email:="", phone:="" )) Dim invNo = GetCellString(ws, r, colMap, "invoice_number") Dim invDate = GetCellString(ws, r, colMap, "invoice_date") If invNo <> "" OrElse invDate <> "" Then za.Documents.Add(New cVERAG_CustomsDeclarations_Document With { .zaDoc_Code = "N380", .zaDoc_Reference = invNo, .zaDoc_Date = invDate, .zaDoc_Description = "Invoice" }) End If dict.Add(grpKey, za) Else za = dict(grpKey) AddOrMergeParty(za.Parties, CreateParty( role:="EXPORTER", eori:=GetCellString(ws, r, colMap, "sender_eori"), name:=GetCellString(ws, r, colMap, "sender_name"), street:=GetCellString(ws, r, colMap, "sender_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "sender_country"), email:="", phone:="" )) AddOrMergeParty(za.Parties, CreateParty( role:="CONSIGNEE", eori:=GetCellString(ws, r, colMap, "recipient_eori"), name:=GetCellString(ws, r, colMap, "recipient_name"), street:=GetCellString(ws, r, colMap, "recipient_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "recipient_country"), email:="", phone:="" )) AddOrMergeParty(za.Parties, CreateParty( role:="IMPORTER", eori:=GetCellString(ws, r, colMap, "importer_eori"), name:=GetCellString(ws, r, colMap, "importer_name"), street:=GetCellString(ws, r, colMap, "importer_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "importer_country"), email:=GetCellString(ws, r, colMap, "importer_email"), phone:=GetCellString(ws, r, colMap, "importer_phone") )) AddOrMergeParty(za.Parties, CreateParty( role:="DECLARANT", eori:=GetCellString(ws, r, colMap, "declarant_eori"), name:=GetCellString(ws, r, colMap, "declarant_name"), street:=GetCellString(ws, r, colMap, "declarant_address"), postalCode:="", city:="", country:=GetCellString(ws, r, colMap, "declarant_country"), email:="", phone:="" )) End If Dim it As New cVERAG_CustomsDeclarations_Item() it.zaItem_PosNo = GetCellInt(ws, r, colMap, "position_no", za.Items.Count + 1) it.zaItem_HSCode = GetCellString(ws, r, colMap, "tariff_code") it.zaItem_OriginCountry = GetCellString(ws, r, colMap, "origin_country") it.zaItem_GrossMass = GetCellDecimalNullable(ws, r, colMap, "gross_mass_kg") it.zaItem_NetMass = GetCellDecimalNullable(ws, r, colMap, "net_mass_kg") it.zaItem_SuppUnitCode = GetCellString(ws, r, colMap, "measurement_unit") it.zaItem_PrevProcedure = GetCellString(ws, r, colMap, "previous_procedure") it.zaItem_MainProcedure = GetCellString(ws, r, colMap, "requested_procedure") it.zaItem_InvoiceValueEUR = GetCellDecimalNullable(ws, r, colMap, "invoice_amount") it.zaItem_InvoiceCurrency = GetCellString(ws, r, colMap, "invoice_currency") it.zaItem_StatisticalValueEUR = GetCellDecimalNullable(ws, r, colMap, "invoice_amount") Dim remarks As String = "" Dim bench = GetCellString(ws, r, colMap, "Benchmark (Default)") Dim emi = GetCellString(ws, r, colMap, "Emission (Default)") Dim fac = GetCellString(ws, r, colMap, "Factor") Dim est = GetCellString(ws, r, colMap, "Estimated Cost") If bench <> "" OrElse emi <> "" OrElse fac <> "" OrElse est <> "" Then remarks = "CBAM Import | " & "Benchmark=" & bench & "; " & "Emission=" & emi & "; " & "Factor=" & fac & "; " & "EstimatedCost=" & est End If it.zaItem_Remarks = remarks Dim itemInvNo = GetCellString(ws, r, colMap, "invoice_number") Dim itemInvDate = GetCellString(ws, r, colMap, "invoice_date") If itemInvNo <> "" OrElse itemInvDate <> "" Then it.Documents.Add(New cVERAG_CustomsDeclarations_Document With { .zaDoc_Code = "N380", .zaDoc_Reference = itemInvNo, .zaDoc_Date = itemInvDate, .zaDoc_Description = "Invoice" }) End If za.Items.Add(it) Next For Each za In dict.Values If za.Items IsNot Nothing AndAlso za.Items.Count > 0 Then za.za_TotGrossMass = za.Items.Sum(Function(x) If(x.zaItem_GrossMass, 0D)) If Not za.za_InvoiceAmount.HasValue Then Dim invSum = za.Items.Sum(Function(x) If(x.zaItem_InvoiceValueEUR, 0D)) If invSum <> 0D Then za.za_InvoiceAmount = invSum End If End If result.Add(za) End If Next End Using Catch ex As Exception VERAG_PROG_ALLGEMEIN.cErrorHandler.ERR(ex.Message, ex.StackTrace, Reflection.MethodInfo.GetCurrentMethod.Name) End Try Return result End Function Private Shared Function ValidateImportRow(ws As IXLWorksheet, row As Integer, colMap As Dictionary(Of String, Integer), ByRef validationMessage As String) As Boolean validationMessage = "" Dim mrn As String = GetCellString(ws, row, colMap, "MRN") Dim tariffCode As String = GetCellString(ws, row, colMap, "tariff_code") Dim declarationDate As Date? = GetCellDateNullable(ws, row, colMap, "declaration_date") Dim missing As New List(Of String) If mrn = "" Then missing.Add("MRN") If Not declarationDate.HasValue Then missing.Add("declaration_date") If tariffCode = "" Then missing.Add("tariff_code") If missing.Count > 0 Then validationMessage = "Zeile " & row & " ungültig. Pflichtfeld(er) fehlen oder sind ungültig: " & String.Join(", ", missing) Return False End If Return True End Function Private Shared Function MRN_EXISTS_IN_DB(mrn As String) As Boolean Try mrn = TrimSafe(mrn) If mrn = "" Then Return False Dim za = cVERAG_CustomsDeclarations.loadByMRN(mrn, False) Return za IsNot Nothing AndAlso za.hasEntry Catch ex As Exception VERAG_PROG_ALLGEMEIN.cErrorHandler.ERR(ex.Message, ex.StackTrace, Reflection.MethodInfo.GetCurrentMethod.Name) End Try Return False End Function Private Shared Function CreateParty(role As String, eori As String, name As String, street As String, postalCode As String, city As String, country As String, email As String, phone As String) As cVERAG_CustomsDeclarations_Parties If TrimSafe(eori) = "" AndAlso TrimSafe(name) = "" AndAlso TrimSafe(street) = "" AndAlso TrimSafe(country) = "" AndAlso TrimSafe(email) = "" AndAlso TrimSafe(phone) = "" Then Return Nothing End If Return New cVERAG_CustomsDeclarations_Parties With { .zaParty_Role = role, .zaParty_EORI = TrimSafe(eori), .zaParty_Name = TrimSafe(name), .zaParty_Street = TrimSafe(street), .zaParty_PostalCode = TrimSafe(postalCode), .zaParty_City = TrimSafe(city), .zaParty_Country = TrimSafe(country), .zaParty_Email = TrimSafe(email), .zaParty_Phone = TrimSafe(phone) } End Function Private Shared Sub AddOrMergeParty(list As List(Of cVERAG_CustomsDeclarations_Parties), p As cVERAG_CustomsDeclarations_Parties) If p Is Nothing Then Exit Sub If list Is Nothing Then Exit Sub Dim existing = list.FirstOrDefault(Function(x) Return String.Equals(TrimSafe(x.zaParty_Role), TrimSafe(p.zaParty_Role), StringComparison.OrdinalIgnoreCase) AndAlso String.Equals(TrimSafe(x.zaParty_EORI), TrimSafe(p.zaParty_EORI), StringComparison.OrdinalIgnoreCase) AndAlso String.Equals(TrimSafe(x.zaParty_Name), TrimSafe(p.zaParty_Name), StringComparison.OrdinalIgnoreCase) End Function) If existing Is Nothing Then list.Add(p) Else If TrimSafe(existing.zaParty_Street) = "" Then existing.zaParty_Street = p.zaParty_Street If TrimSafe(existing.zaParty_PostalCode) = "" Then existing.zaParty_PostalCode = p.zaParty_PostalCode If TrimSafe(existing.zaParty_City) = "" Then existing.zaParty_City = p.zaParty_City If TrimSafe(existing.zaParty_Country) = "" Then existing.zaParty_Country = p.zaParty_Country If TrimSafe(existing.zaParty_Email) = "" Then existing.zaParty_Email = p.zaParty_Email If TrimSafe(existing.zaParty_Phone) = "" Then existing.zaParty_Phone = p.zaParty_Phone End If End Sub Private Shared Function GetCellString(ws As IXLWorksheet, row As Integer, colMap As Dictionary(Of String, Integer), header As String) As String Try If ws Is Nothing OrElse colMap Is Nothing Then Return "" If Not colMap.ContainsKey(header) Then Return "" Return TrimSafe(ws.Cell(row, colMap(header)).GetString()) Catch Return "" End Try End Function Private Shared Function GetCellDecimalNullable(ws As IXLWorksheet, row As Integer, colMap As Dictionary(Of String, Integer), header As String) As Decimal? Try If ws Is Nothing OrElse colMap Is Nothing Then Return Nothing If Not colMap.ContainsKey(header) Then Return Nothing Dim cell = ws.Cell(row, colMap(header)) If cell Is Nothing OrElse cell.IsEmpty() Then Return Nothing If cell.DataType = XLDataType.Number Then Return Convert.ToDecimal(cell.GetDouble()) End If Dim txt = TrimSafe(cell.GetString()) If txt = "" Then Return Nothing Dim d As Decimal txt = txt.Replace(" ", "") If Decimal.TryParse(txt, NumberStyles.Any, CultureInfo.InvariantCulture, d) Then Return d If Decimal.TryParse(txt, NumberStyles.Any, CultureInfo.GetCultureInfo("de-AT"), d) Then Return d If Decimal.TryParse(txt, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), d) Then Return d Catch End Try Return Nothing End Function Private Shared Function GetCellDateNullable(ws As IXLWorksheet, row As Integer, colMap As Dictionary(Of String, Integer), header As String) As Date? Try If ws Is Nothing OrElse colMap Is Nothing Then Return Nothing If Not colMap.ContainsKey(header) Then Return Nothing Dim cell = ws.Cell(row, colMap(header)) If cell Is Nothing OrElse cell.IsEmpty() Then Return Nothing If cell.DataType = XLDataType.DateTime Then Return cell.GetDateTime() End If If cell.DataType = XLDataType.Number Then Return DateTime.FromOADate(cell.GetDouble()) End If Dim txt = TrimSafe(cell.GetString()) If txt = "" Then Return Nothing Dim dt As DateTime If DateTime.TryParse(txt, CultureInfo.InvariantCulture, DateTimeStyles.None, dt) Then Return dt If DateTime.TryParse(txt, CultureInfo.GetCultureInfo("de-AT"), DateTimeStyles.None, dt) Then Return dt If DateTime.TryParse(txt, CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.None, dt) Then Return dt Catch End Try Return Nothing End Function Private Shared Function GetCellInt(ws As IXLWorksheet, row As Integer, colMap As Dictionary(Of String, Integer), header As String, Optional defaultValue As Integer = 0) As Integer Try If ws Is Nothing OrElse colMap Is Nothing Then Return defaultValue If Not colMap.ContainsKey(header) Then Return defaultValue Dim cell = ws.Cell(row, colMap(header)) If cell Is Nothing OrElse cell.IsEmpty() Then Return defaultValue If cell.DataType = XLDataType.Number Then Return Convert.ToInt32(Math.Truncate(cell.GetDouble())) End If Dim txt = TrimSafe(cell.GetString()) If txt = "" Then Return defaultValue Dim i As Integer If Integer.TryParse(txt, i) Then Return i Dim d As Decimal If Decimal.TryParse(txt, NumberStyles.Any, CultureInfo.InvariantCulture, d) Then Return Convert.ToInt32(Math.Truncate(d)) End If If Decimal.TryParse(txt, NumberStyles.Any, CultureInfo.GetCultureInfo("de-AT"), d) Then Return Convert.ToInt32(Math.Truncate(d)) End If If Decimal.TryParse(txt, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), d) Then Return Convert.ToInt32(Math.Truncate(d)) End If Catch End Try Return defaultValue End Function Private Shared Function TrimSafe(value As Object) As String If value Is Nothing Then Return "" Return Convert.ToString(value).Trim() End Function End Class