Imports System.Collections.Generic Imports System.Data Imports System.IO Imports DigitalData.Modules.Base Imports DigitalData.Modules.Database Imports DigitalData.Modules.Logging Imports GdPicture14 Imports System.Drawing Imports System.Linq Imports System.Text.RegularExpressions '11.11.2025 Public Class XRechnungViewDocument Private ReadOnly _logger As Logger Private ReadOnly _logConfig As LogConfig Private ReadOnly _filesystem As FilesystemEx Private ReadOnly _file As ZUGFeRD.FileFunctions Private ReadOnly _gdpictureLicenseKey As String Private fontResName As String Private fontResNameBold As String Private fontResNameItalic As String ' Layout-Konstanten Private Const MARGIN_LEFT As Integer = 10 Private Const MARGIN_TOP As Integer = 15 Private Const LINE_WIDTH As Integer = 200 Private Const PAGE_HEIGHT_LIMIT As Integer = 270 Private Const FOOTER_Y As Integer = 280 Private Const FOOTER_TEXT_Y As Integer = 285 Private Const LINE_HEIGHT As Integer = 5 ' Spalten-Positionen für Tabellen Private Const COL_POS_NUMBER As Integer = 10 Private Const COL_POS_AMOUNT As Integer = 19 Private Const COL_POS_UNIT As Integer = 35 Private Const COL_POS_TEXT As Integer = 50 Private Const COL_POS_REASON As Integer = 20 Private Const COL_POS_TAX As Integer = 163 Private Const COL_POS_SUM As Integer = 181 Private Const COL_VALUE_X As Integer = 70 ' Text-Größen Private Const TEXT_SIZE_TITLE As Integer = 18 Private Const TEXT_SIZE_NORMAL As Integer = 10 ' Text-Längen für Umbruch Private Const MAX_TEXT_LENGTH_FULL As Integer = 112 Private Const MAX_TEXT_LENGTH_POSITION As Integer = 64 Private Const MAX_TEXT_LENGTH_NOTE As Integer = 70 Public Sub New(LogConfig As LogConfig, MSSQL As MSSQLServer, GDPictureLicenseKey As String) _logConfig = LogConfig _logger = LogConfig.GetLogger() _filesystem = New FilesystemEx(_logConfig) _file = New ZUGFeRD.FileFunctions(LogConfig, MSSQL) _gdpictureLicenseKey = GDPictureLicenseKey End Sub Public Function Create_PDFfromXML(pXmlFile As FileInfo, pDTItemValues As DataTable) As FileInfo _logger.Debug("Create_PDFfromXML() Start") Try ' 1. Initialisierung der Dateipfade Dim paths As FilePaths = InitializeFilePaths(pXmlFile) If paths Is Nothing Then Return Nothing ' 2. PDF erstellen und konfigurieren Dim pdfDoc As GdPicturePDF = InitializePDF() If pdfDoc Is Nothing Then Return Nothing ' 3. Rendering-Context erstellen Dim context As New PdfRenderContext(pdfDoc, paths.CreatedString) ' 4. Erste Seite mit Header/Footer CreateNewPage(context) ' 5. Daten verarbeiten und rendern ProcessInvoiceData(context, pDTItemValues) ' 6. XML einbetten und speichern Return FinalizePDF(pdfDoc, paths) Catch ex As Exception _logger.Error(ex) Return Nothing End Try End Function #Region "Initialisierung" Private Function InitializeFilePaths(xmlFile As FileInfo) As FilePaths Try Dim xmlPath As String = xmlFile.FullName Dim tempPath As String = Path.Combine(Path.GetDirectoryName(xmlPath), "temp") ' Temp-Verzeichnis erstellen If Not Directory.Exists(tempPath) Then Directory.CreateDirectory(tempPath) End If Dim tempXmlPath As String = Path.Combine(tempPath, "xrechnung.xml") If File.Exists(tempXmlPath) Then File.Delete(tempXmlPath) End If Dim pdfFilename As String = Regex.Replace(xmlFile.Name, ".xml", ".pdf", RegexOptions.IgnoreCase) Dim outputPath As String = Path.Combine(Path.GetDirectoryName(xmlPath), pdfFilename) If File.Exists(outputPath) Then File.Delete(outputPath) End If Dim paths As New FilePaths With { .XmlPath = xmlPath, .TempPath = tempPath, .TempXmlPath = tempXmlPath, .PdfFilename = pdfFilename, .OutputPath = outputPath, .CreatedString = $"Maschinell erstellt durch / Automatically created by Digital Data E-Rechnung Parser: {Now.ToString}" } _logger.Debug("Create_PDFfromXML() Resulting PDF Filepath: [{0}]", paths.OutputPath) Return paths Catch ex As Exception _logger.Error("Error initializing file paths", ex) Return Nothing End Try End Function Private Function InitializePDF() As GdPicturePDF Try Dim licManager As New LicenseManager() licManager.RegisterKEY(_gdpictureLicenseKey) Dim pdf As New GdPicturePDF() Dim status As GdPictureStatus = pdf.NewPDF(PdfConformance.PDF_A_3a) If status <> GdPictureStatus.OK Then _logger.Warn($"Error initializing PDF: {status}") Return Nothing End If ' PDF-Einstellungen pdf.SetOrigin(PdfOrigin.PdfOriginTopLeft) pdf.SetMeasurementUnit(PdfMeasurementUnit.PdfMeasurementUnitMillimeter) pdf.SetLineWidth(1) pdf.SetTitle("xInvoice VisualReceipt") pdf.SetAuthor("Digital Data GmbH, Ludwig Rinn Str. 16, 35452 Heuchelheim") ' Fonts initialisieren fontResName = pdf.AddStandardFont(PdfStandardFont.PdfStandardFontHelvetica) fontResNameBold = pdf.AddStandardFont(PdfStandardFont.PdfStandardFontHelveticaBold) fontResNameItalic = pdf.AddStandardFont(PdfStandardFont.PdfStandardFontHelveticaBoldOblique) Return pdf Catch ex As Exception _logger.Error("Error initializing PDF", ex) Return Nothing End Try End Function #End Region #Region "Seiten-Management" Private Sub CreateNewPage(context As PdfRenderContext) Dim status As GdPictureStatus = context.PDF.NewPage(PdfPageSizes.PdfPageSizeA4) If status <> GdPictureStatus.OK Then Throw New Exception($"Could not create page: {status}") End If DrawHeader(context.PDF) DrawFooter(context.PDF, context.CreatedString) context.YPosition = MARGIN_TOP + 30 ' Nach Header End Sub Private Sub DrawHeader(pdf As GdPicturePDF) Dim y As Integer = MARGIN_TOP pdf.SetTextSize(TEXT_SIZE_TITLE) pdf.DrawText(fontResName, MARGIN_LEFT, y, "xRechnung Sichtbeleg - xInvoice Visual Receipt") y += 10 pdf.SetTextSize(TEXT_SIZE_NORMAL) pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_DE_Row1) y += LINE_HEIGHT pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_DE_Row2) y += LINE_HEIGHT pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_EN_Row1) y += LINE_HEIGHT pdf.DrawText(fontResNameItalic, MARGIN_LEFT, y, XRechnungStrings.CommentSichtbeleg_EN_Row2) End Sub Private Sub DrawFooter(pdf As GdPicturePDF, createdString As String) pdf.DrawLine(MARGIN_LEFT, FOOTER_Y, LINE_WIDTH, FOOTER_Y) pdf.DrawText(fontResName, MARGIN_LEFT, FOOTER_TEXT_Y, createdString) End Sub Private Sub CheckAndCreateNewPageIfNeeded(context As PdfRenderContext) If context.YPosition >= PAGE_HEIGHT_LIMIT Then Dim status As GdPictureStatus = context.PDF.NewPage(PdfPageSizes.PdfPageSizeA4) If status <> GdPictureStatus.OK Then _logger.Warn($"Could not create a second page. The error was: {status}") Throw New Exception($"Could not create page: {status}") End If DrawHeader(context.PDF) DrawFooter(context.PDF, context.CreatedString) context.YPosition = MARGIN_TOP + 30 End If End Sub #End Region #Region "Datenverarbeitung" Private Sub ProcessInvoiceData(context As PdfRenderContext, dataTable As DataTable) Dim formerItemSpecName As String = "" For Each oRow As DataRow In dataTable.Rows ' Prüfen ob neue Seite benötigt wird CheckAndCreateNewPageIfNeeded(context) Dim itemData As New InvoiceItemData(oRow) ' Interne Zeilen behandeln If itemData.NormalizedArea = "INTERNAL" Then HandleInternalRow(context, itemData) Continue For End If _logger.Debug($"WorkingItem: Area=[{itemData.NormalizedArea}] SpecName=[{itemData.SpecName}] Value=[{itemData.Value}] Caption=[{itemData.Caption}] Display=[{itemData.Display}] LastRow=[{itemData.IsLastRowSameArea}]") ' WICHTIG: Area-Switch-Flag VORHER setzen! Dim isAreaSwitch As Boolean = (context.CurrentArea <> itemData.NormalizedArea) ' Area-Wechsel behandeln If isAreaSwitch Then formerItemSpecName = "" ' Reset bei Area-Wechsel HandleAreaSwitch(context, itemData) Else HandleFollowUpItem(context, itemData, formerItemSpecName) End If ' Item anzeigen - MIT Area-Switch-Info übergeben! If itemData.Display AndAlso Not String.IsNullOrEmpty(itemData.Value) Then RenderDisplayItem(context, itemData, isAreaSwitch) ' ← Parameter hinzugefügt! End If Next End Sub Private Sub HandleInternalRow(context As PdfRenderContext, itemData As InvoiceItemData) _logger.Debug("Next Item as Area is internal") If itemData.SpecName = "STATIC_Y_SWITCH" Then context.YPosition = CInt(itemData.Value) End If End Sub #End Region #Region "Area-Switch Handling" Private Sub HandleAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) ' WICHTIG: CurrentArea speichert die NORMALISIERTE Area context.CurrentArea = itemData.NormalizedArea context.CreateTextBox = False _logger.Debug($"Area-Switch to: {context.CurrentArea}") ' Area-Header zeichnen - ÜBERGIBT itemData komplett Dim areaCaption As String = GetAreaCaption(context, itemData) If Not String.IsNullOrEmpty(areaCaption) Then DrawAreaHeader(context, areaCaption) DrawAreaSpecificHeaders(context) End If ' Area-spezifische Initialisierung - VERWENDET context.CurrentArea (normalisiert) Select Case context.CurrentArea Case "TYPE" HandleTypeAreaSwitch(context, itemData) Case "POSITION" HandlePositionAreaSwitch(context, itemData) Case "ALLOWANCE" HandleAllowanceAreaSwitch(context, itemData) Case "INCL_NOTE" HandleIncludedNoteAreaSwitch(context, itemData) Case "TAXPOS" HandleTaxPosAreaSwitch(context, itemData) Case "AMOUNT" context.CreateTextBox = True End Select End Sub Private Function GetAreaCaption(context As PdfRenderContext, itemData As InvoiceItemData) As String ' VERWENDET NormalizedArea für Switch, aber AreaCaptionFromPattern für INCL_NOTE Select Case itemData.NormalizedArea Case "TYPE" Return $"{Return_InvType(itemData.Value)} [{itemData.Value}]" Case "SELLER" Return "Verkäufer / Seller:" Case "BUYER" Return "Käufer / Buyer:" Case "HEAD" Return String.Empty Case "POSITION" Return "Positionen / Positions:" Case "INCL_NOTE" ' Verwendet den Caption aus dem Pattern INCL_NOTE#... If Not String.IsNullOrEmpty(itemData.AreaCaptionFromPattern) Then Return itemData.AreaCaptionFromPattern End If ' Fallback wenn kein Pattern vorhanden Return "Notizen und Hinweise / Notes:" Case "ALLOWANCE" Return GetAllowanceCaption(context, itemData) Case "AMOUNT" Return "Beträge / Amounts:" Case "TAXPOS" Return "Steuerbeträge / Tax amounts:" Case "PAYMENT" Return "Zahlungsinformationen / Payment details:" Case "EXEMPTION" Return "UST.-Befreiungsgrund / Exemption reason:" Case Else Return String.Empty End Select End Function Private Function GetAllowanceCaption(context As PdfRenderContext, itemData As InvoiceItemData) As String If itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then If itemData.Value = "False" Then context.HasDiscount = True Return "Rabatt/Discount:" Else Return "Zuschlag/Surcharge:" End If End If Return "Zu- oder Abschlag/Surcharge or Discount:" End Function Private Sub DrawAreaHeader(context As PdfRenderContext, caption As String) context.YPosition += LINE_HEIGHT context.PDF.DrawLine(MARGIN_LEFT, context.YPosition, LINE_WIDTH, context.YPosition) context.YPosition += LINE_HEIGHT context.PDF.DrawText(fontResNameBold, MARGIN_LEFT, context.YPosition, caption) context.YPosition += LINE_HEIGHT If context.CurrentArea = "TYPE" Then context.PDF.DrawLine(MARGIN_LEFT, context.YPosition, LINE_WIDTH, context.YPosition) context.YPosition += LINE_HEIGHT End If End Sub Private Sub DrawAreaSpecificHeaders(context As PdfRenderContext) 'VERWENDET context.CurrentArea (bereits normalisiert) Select Case context.CurrentArea Case "POSITION" DrawPositionTableHeader(context) Case "INCL_NOTE" ' Kein spezifischer Header für INCL_NOTE mehr nötig ' Der Caption kommt bereits aus GetAreaCaption Case "ALLOWANCE" ' ← KORRIGIERT! DrawAllowanceTableHeader(context) End Select End Sub Private Sub DrawPositionTableHeader(context As PdfRenderContext) context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "Pos#") context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, "Anz./am.") context.PDF.DrawText(fontResName, COL_POS_UNIT, context.YPosition, "Einh/Unit") context.PDF.DrawText(fontResName, COL_POS_TEXT, context.YPosition, "Pos.Text") context.PDF.DrawText(fontResName, COL_POS_TAX, context.YPosition, "Steuer/Tax") context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, "Betrag/Sum") End Sub Private Sub DrawAllowanceTableHeader(context As PdfRenderContext) context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "Pos#") context.PDF.DrawText(fontResName, COL_POS_REASON, context.YPosition, "Grund/Reason") context.PDF.DrawText(fontResName, COL_POS_TAX, context.YPosition, "Steuer/Tax") context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, "Betrag/Sum") context.PositionCount = 0 End Sub #End Region #Region "Area-Specific Switch Handlers" Private Sub HandleTypeAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) If itemData.SpecName = "INVOICE_CURRENCY" Then If itemData.Value <> "EUR" Then context.CurrencySymbol = itemData.Value End If End If End Sub Private Sub HandlePositionAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) If itemData.SpecName = "INVOICE_POSITION_AMOUNT" Then ' KEIN Increment hier - wird in Follow-Up gemacht context.YDynamic = 0 context.YPosition += LINE_HEIGHT ' Neue Zeile für erste Position context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "1") ' Erste Position ist immer 1 context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, itemData.Value) context.PositionCount = 1 ' Zähler auf 1 setzen statt increment itemData.Display = False End If End Sub Private Sub HandleAllowanceAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) ' Erste Allowance: Prüfe ob es CHARGE_INDICATOR (Metadata) oder ACTUAL_AMOUNT (erste Zeile) ist If itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then ' Nur Metadata - nichts rendern, nur Flag setzen _logger.Debug($"Found RECEIPT_ALLOWANCE_CHARGE_INDICATOR with value [{itemData.Value}]. Setting HasDiscount={itemData.Value = "False"} in context.") itemData.Display = False ElseIf {"RECEIPT_ALLOWANCE_ACTUAL_AMOUNT", "POSITION_ALLOWANCE_ACTUAL_AMOUNT"}.Contains(itemData.SpecName) Then ' Erste Allowance-Zeile context.YPosition += LINE_HEIGHT context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, "1") Dim currTerm As String = FormatCurrency(itemData.Value, context.CurrencySymbol) If context.HasDiscount AndAlso itemData.SpecName = "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT" AndAlso Not currTerm.StartsWith("-") Then currTerm = "-" + currTerm End If context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, currTerm) ' ← In COL_POS_SUM! context.PositionCount = 1 itemData.Display = False _logger.Debug($"First Allowance/Charge rendered with amount [{currTerm}]. PositionCount set to 1. HasDiscount={context.HasDiscount}. Display set to False.") End If End Sub Private Sub HandleIncludedNoteAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) ' Erste Note direkt ausgeben context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, itemData.Value) itemData.Display = False End Sub Private Sub HandleTaxPosAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) If itemData.SpecName = "INVOICE_TAXPOS_RATE" Then context.PositionCount = 1 context.TaxPosText = $"{itemData.Value} %: " ' ← In Context speichern! context.IsFirstTaxPosDisplay = True ' ← Flag setzen! itemData.Display = False _logger.Debug($"TAXPOS RATE in AreaSwitch accumulated: [{context.TaxPosText}]") End If End Sub #End Region #Region "Follow-Up Item Handling" Private Sub HandleFollowUpItem(context As PdfRenderContext, itemData As InvoiceItemData, ByRef formerItemSpecName As String) _logger.Debug($"FollowItem START - CurrentArea: [{context.CurrentArea}] - ItemArea: [{itemData.NormalizedArea}] - SpecName: [{itemData.SpecName}] - Value: [{itemData.Value}] - YPos: [{context.YPosition}]") Dim descriptionFollowup As Boolean = False Select Case context.CurrentArea Case "POSITION", "ALLOWANCE" _logger.Debug($"FollowItem: Entering POSITION/ALLOWANCE handler") HandlePositionOrAllowanceFollowUp(context, itemData, formerItemSpecName, descriptionFollowup) Case "HEAD" _logger.Debug($"FollowItem: Entering HEAD handler") HandleHeadFollowUp(itemData) Case "TAXPOS" _logger.Debug($"FollowItem: Entering TAXPOS handler") HandleTaxPosFollowUp(context, itemData) Case "INCL_NOTE" _logger.Debug($"FollowItem: Entering INCL_NOTE handler - YPosition before={context.YPosition}") context.YPosition += LINE_HEIGHT context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, itemData.Value) itemData.Display = False _logger.Debug($"FollowItem: INCL_NOTE handler - YPosition after={context.YPosition}, Display set to False") Case Else _logger.Warn($"FollowItem: UNHANDLED CurrentArea=[{context.CurrentArea}] for SpecName=[{itemData.SpecName}]") End Select _logger.Debug($"FollowItem END - CurrentArea: [{context.CurrentArea}] - YPos: [{context.YPosition}] - Display: [{itemData.Display}]") End Sub Private Sub HandlePositionOrAllowanceFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, ByRef formerItemSpecName As String, ByRef descriptionFollowup As Boolean) ' Former Item Tracking If itemData.SpecName <> formerItemSpecName AndAlso formerItemSpecName <> "" Then If itemData.SpecName = "INVOICE_POSITION_ARTICLE_DESCRIPTION" AndAlso formerItemSpecName = "INVOICE_POSITION_ARTICLE" Then descriptionFollowup = True Else formerItemSpecName = itemData.SpecName End If Else formerItemSpecName = itemData.SpecName End If ' Spezifische Item-Behandlung If {"INVOICE_POSITION_AMOUNT", "POSITION_ALLOWANCE_ACTUAL_AMOUNT", "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT"}.Contains(itemData.SpecName) Then HandlePositionAmountFollowUp(context, itemData, descriptionFollowup) ElseIf itemData.SpecName = "INVOICE_POSITION_UNIT_TYPE" Then HandleUnitTypeFollowUp(context, itemData) ElseIf {"POSITION_ALLOWANCE_REASON", "RECEIPT_ALLOWANCE_REASON"}.Contains(itemData.SpecName) Then ' ALLOWANCE_REASON direkt in Spalte schreiben context.PDF.DrawText(fontResName, COL_POS_REASON, context.YPosition, itemData.Value) itemData.Display = False ElseIf {"INVOICE_POSITION_ARTICLE", "INVOICE_POSITION_ARTICLE_DESCRIPTION"}.Contains(itemData.SpecName) Then HandleArticleTextFollowUp(context, itemData, descriptionFollowup) ElseIf itemData.SpecName = "INVOICE_POSITION_NOTE" Then HandlePositionNoteFollowUp(context, itemData) ElseIf {"INVOICE_TAXPOS_TAX_RATE", "INVOICE_TAXPOS_RATE"}.Contains(itemData.SpecName) Then ' ← NUR für POSITION: Tax Rate HandleTaxRateFollowUp(context, itemData) ElseIf {"RECEIPT_ALLOWANCE_VAT_RATE", "POSITION_ALLOWANCE_VAT_RATE"}.Contains(itemData.SpecName) Then ' ← NEU: Für ALLOWANCE: VAT Rate (nicht CALCULATION_PERCENT!) HandleTaxRateFollowUp(context, itemData) ElseIf itemData.SpecName = "INVOICE_POSITION_TAX_AMOUNT" Then HandlePositionTaxAmountFollowUp(context, itemData) ElseIf {"RECEIPT_ALLOWANCE_VAT_CODE", "POSITION_ALLOWANCE_VAT_CODE"}.Contains(itemData.SpecName) Then ' VAT_CODE wird nicht angezeigt (nur Metadata) itemData.Display = False ElseIf {"RECEIPT_ALLOWANCE_CALCULATION_PERCENT", "POSITION_ALLOWANCE_CALCULATION_PERCENT"}.Contains(itemData.SpecName) Then ' ← NEU: CALCULATION_PERCENT wird nicht angezeigt (nur Metadata) itemData.Display = False ElseIf itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then ' CHARGE_INDICATOR im Follow-Up (zweite Allowance) wird nicht angezeigt itemData.Display = False End If End Sub Private Sub HandlePositionAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean) context.PositionCount += 1 If Not descriptionFollowup Then context.YPlus = 0 context.YDynamic = 0 End If ' WICHTIG: Neue Zeile für jede neue Position! context.YPosition += LINE_HEIGHT context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, context.PositionCount.ToString()) If itemData.SpecName = "INVOICE_POSITION_AMOUNT" Then context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, itemData.Value) ElseIf {"POSITION_ALLOWANCE_ACTUAL_AMOUNT", "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT"}.Contains(itemData.SpecName) Then Dim term As String = FormatCurrency(itemData.Value, context.CurrencySymbol) If context.HasDiscount AndAlso itemData.SpecName = "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT" AndAlso Not term.StartsWith("-") Then term = "-" + term End If context.PDF.DrawText(fontResName, COL_POS_SUM, context.YPosition, term) Else If context.YDynamic = 0 Then context.YDynamic = context.YPosition End If RenderMultiLineText(context, itemData.Value, COL_POS_AMOUNT, MAX_TEXT_LENGTH_POSITION) End If itemData.Display = False End Sub Private Sub HandleUnitTypeFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) context.YPlus = 0 Dim unit As String = Return_UnitType(itemData.Value) context.PDF.DrawText(fontResName, COL_POS_UNIT, context.YPosition, unit) itemData.Display = False End Sub Private Sub HandleArticleTextFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean) If Not descriptionFollowup Then context.YPlus = 0 End If If context.YDynamic = 0 Then context.YDynamic = context.YPosition End If Dim xPos As Integer = COL_POS_TEXT If itemData.SpecName.Contains("ALLOWANCE") Then xPos = COL_POS_REASON End If RenderMultiLineText(context, itemData.Value, xPos, MAX_TEXT_LENGTH_POSITION) itemData.Display = False End Sub Private Sub HandlePositionNoteFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) If context.YDynamic = 0 Then context.YDynamic = context.YPosition End If RenderMultiLineText(context, itemData.Value, COL_POS_TEXT, MAX_TEXT_LENGTH_NOTE) itemData.Display = False End Sub Private Sub HandleTaxRateFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) context.PDF.DrawText(fontResName, COL_POS_TAX, context.YPosition, $"{itemData.Value} %") itemData.Display = False End Sub Private Sub HandlePositionTaxAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) Dim yPos As Double = context.YPosition - 3.5 Dim taxTerm As String = FormatCurrency(itemData.Value, context.CurrencySymbol) context.PDF.DrawTextBox(fontResName, 177, yPos, 198, YCoo_TextBoxPlus5(yPos), TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentNear, taxTerm) itemData.Display = False ' WICHTIG: Display auf False setzen! End Sub Private Sub HandleHeadFollowUp(itemData As InvoiceItemData) If {"INVOICE_DATE", "INVOICE_SERVICE_DATE"}.Contains(itemData.SpecName) Then itemData.Value = StringFunctions.DatetimeStringToGermanStringConverter(itemData.Value, _logger) End If End Sub Private Sub HandleTaxPosFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) ' TAXPOS Items werden zu einem String kombiniert If itemData.SpecName = "INVOICE_TAXPOS_RATE" Then context.PositionCount += 1 context.TaxPosText = $"{itemData.Value} %: " ' Speichern statt direkt setzen itemData.Display = False _logger.Debug($"TAXPOS RATE accumulated: [{context.TaxPosText}]") ElseIf itemData.SpecName = "INVOICE_TAXPOS_AMOUNT" Then Dim amount As String = FormatCurrency(itemData.Value, context.CurrencySymbol) context.TaxPosText &= amount ' Anhängen itemData.Display = False _logger.Debug($"TAXPOS AMOUNT accumulated: [{context.TaxPosText}]") ElseIf itemData.SpecName = "INVOICE_TAXPOS_TYPE" Then context.TaxPosText &= $" {itemData.Value}" ' Anhängen itemData.Value = context.TaxPosText ' JETZT den kombinierten String setzen itemData.Display = True ' Und anzeigen! context.TaxPosText = "" ' Reset für nächste TAXPOS _logger.Debug($"TAXPOS TYPE final: [{itemData.Value}], Display=True") ElseIf itemData.Value.Contains("EXEMPTION") Then _logger.Debug($"We got an Exemption: {itemData.Value}") End If End Sub #End Region #Region "Rendering" Private Sub RenderDisplayItem(context As PdfRenderContext, itemData As InvoiceItemData, isAreaSwitch As Boolean) ' Spezielle Behandlung für TAXPOS: Erstes Display-Item nach Area-Switch Dim skipLineHeight As Boolean = isAreaSwitch OrElse context.IsFirstTaxPosDisplay If context.IsFirstTaxPosDisplay Then context.IsFirstTaxPosDisplay = False ' Reset nach Verwendung _logger.Debug($"RenderDisplayItem: TAXPOS first display item - skipping line height") End If ' Y-Position anpassen - ABER NICHT nach Area-Switch oder erstem TAXPOS Display! If Not itemData.IsLastRowSameArea AndAlso Not skipLineHeight Then context.YPosition += LINE_HEIGHT _logger.Debug($"RenderDisplayItem: Adding line height. New YPosition: {context.YPosition}") End If ' Formatierung für Währungsfelder Dim displayValue As String = itemData.Value If ShouldFormatAsCurrency(context.CurrentArea, itemData.SpecName) Then displayValue = FormatCurrency(itemData.Value, context.CurrencySymbol) End If ' Rendern basierend auf Layout If Not String.IsNullOrEmpty(itemData.Caption) Then RenderLabelValuePair(context, itemData.Caption, displayValue) Else If itemData.IsLastRowSameArea Then context.PDF.DrawText(fontResName, itemData.XPosition, context.YPosition, displayValue) Else RenderTextWithWrapping(context, displayValue) End If End If End Sub Private Function ShouldFormatAsCurrency(area As String, specName As String) As Boolean If area <> "AMOUNT" AndAlso area <> "ALLOWANCE" Then Return False Dim currencyFields As String() = { "INVOICE_TOTAL_TAX", "INVOICE_TOTAL_NET", "INVOICE_TOTAL_GROSS", "INVOICE_TOTAL_CHARGE_AMOUNT", "POSITION_ALLOWANCE_ACTUAL_AMOUNT", "RECEIPT_ALLOWANCE_ACTUAL_AMOUNT", "POSITION_ALLOWANCE_CALCULATION_PERCENT", "RECEIPT_ALLOWANCE_CALCULATION_PERCENT" } Return currencyFields.Contains(specName) End Function Private Sub RenderLabelValuePair(context As PdfRenderContext, label As String, value As String) context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, label) If context.CreateTextBox Then Dim textBoxYPos As Integer = context.YPosition - 3 context.PDF.DrawTextBox(fontResName, COL_VALUE_X, textBoxYPos, 90, YCoo_TextBoxPlus5(textBoxYPos), TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentCenter, value) Else context.PDF.DrawText(fontResName, COL_VALUE_X, context.YPosition, value) End If End Sub Private Sub RenderTextWithWrapping(context As PdfRenderContext, text As String) If text.Length > MAX_TEXT_LENGTH_FULL Then For i As Integer = 0 To text.Length - 1 Step MAX_TEXT_LENGTH_FULL Dim length As Integer = Math.Min(MAX_TEXT_LENGTH_FULL, text.Length - i) Dim part As String = text.Substring(i, length) context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, part) context.YPosition += LINE_HEIGHT Next Else context.PDF.DrawText(fontResName, MARGIN_LEFT, context.YPosition, text) End If End Sub Private Sub RenderMultiLineText(context As PdfRenderContext, text As String, xPos As Integer, maxLength As Integer) Dim partsNL As List(Of String) = StringFunctions.SplitTextByNewLine(text) For Each linePart As String In partsNL Dim parts As List(Of String) = StringFunctions.SplitText_Length(linePart, maxLength) For Each part As String In parts context.PDF.DrawText(fontResName, xPos, context.YDynamic, part) context.YDynamic += LINE_HEIGHT context.YPlus += LINE_HEIGHT Next Next End Sub #End Region #Region "Finalisierung" Private Function FinalizePDF(pdf As GdPicturePDF, paths As FilePaths) As FileInfo Try ' XML einbetten If File.Exists(paths.XmlPath) Then pdf.EmbedFile(paths.XmlPath, "E-invoice XML attachment") Else _logger.Info("XML File is not existing and could not be embedded!") Return Nothing End If pdf.EnableCompression(True) ' Speichern Dim status As GdPictureStatus = pdf.SaveToFile(paths.OutputPath) If status = GdPictureStatus.OK Then _logger.Info("PDF VisualReceipt generated successfully!") _logger.Debug("Vor MOVE... oxmlFilePath: [{0}] / oTempFilePath: [{1}]", paths.XmlPath, paths.TempXmlPath) File.Move(paths.XmlPath, paths.TempXmlPath) pdf.CloseDocument() Dim result As New FileInfo(paths.OutputPath) _logger.Info("Create_PDFfromXML() End successfully. File [{0}] written.", result.FullName) Return result Else _logger.Warn($"Error generating PDF VisualReceipt: {status}") pdf.CloseDocument() Return Nothing End If Catch ex As Exception pdf.CloseDocument() _logger.Error("Error finalizing PDF", ex) Return Nothing End Try End Function #End Region #Region "Helper Classes" Private Class FilePaths Public Property XmlPath As String Public Property TempPath As String Public Property TempXmlPath As String Public Property PdfFilename As String Public Property OutputPath As String Public Property CreatedString As String End Class Private Class PdfRenderContext Public Property PDF As GdPicturePDF Public Property YPosition As Integer Public Property CurrentArea As String Public Property CurrencySymbol As String Public Property PositionCount As Integer Public Property HasDiscount As Boolean Public Property YDynamic As Integer Public Property YPlus As Integer Public Property CreateTextBox As Boolean Public Property CreatedString As String Public Property TaxPosText As String Public Property IsFirstTaxPosDisplay As Boolean ' ← NEU Public Sub New(pdf As GdPicturePDF, createdString As String) Me.PDF = pdf Me.CreatedString = createdString YPosition = MARGIN_TOP CurrentArea = String.Empty CurrencySymbol = "€" PositionCount = 0 HasDiscount = False YDynamic = 0 YPlus = 0 CreateTextBox = False TaxPosText = "" IsFirstTaxPosDisplay = False ' ← NEU End Sub End Class Private Class InvoiceItemData Public Property Area As String Public Property SpecName As String Public Property Value As String Public Property Caption As String Public Property Display As Boolean Public Property IsLastRowSameArea As Boolean Public Property XPosition As Integer Public Property AreaSwitch As Boolean ' Neue Property für normalisierte Area Public ReadOnly Property NormalizedArea As String Get Return NormalizeAreaName(Area) End Get End Property ' Neue Property für Area-Caption (nur bei INCL_NOTE#) Public ReadOnly Property AreaCaptionFromPattern As String Get If Area.StartsWith("INCL_NOTE#") Then Return Area.Substring(10) ' Entfernt "INCL_NOTE#" End If Return String.Empty End Get End Property Public Sub New(row As DataRow) Area = GetSafeString(row, "Area") SpecName = GetSafeString(row, "SPEC_NAME") Value = GetSafeString(row, "ITEM_VALUE") Caption = GetSafeString(row, "Row_Caption") Display = GetSafeBoolean(row, "Display", False) IsLastRowSameArea = GetSafeBoolean(row, "Y_eq_lastrow", False) XPosition = GetSafeInteger(row, "xPosition", MARGIN_LEFT) AreaSwitch = False End Sub Private Shared Function GetSafeString(row As DataRow, columnName As String) As String If row.Table.Columns.Contains(columnName) AndAlso Not IsDBNull(row.Item(columnName)) Then Return row.Item(columnName).ToString() End If Return String.Empty End Function Private Shared Function GetSafeBoolean(row As DataRow, columnName As String, defaultValue As Boolean) As Boolean If row.Table.Columns.Contains(columnName) AndAlso Not IsDBNull(row.Item(columnName)) Then Return CBool(row.Item(columnName)) End If Return defaultValue End Function Private Shared Function GetSafeInteger(row As DataRow, columnName As String, defaultValue As Integer) As Integer If row.Table.Columns.Contains(columnName) AndAlso Not IsDBNull(row.Item(columnName)) Then Return CInt(row.Item(columnName)) End If Return defaultValue End Function ' Statische Hilfsmethode für Area-Normalisierung Private Shared Function NormalizeAreaName(area As String) As String If area.StartsWith("INCL_NOTE#") Then Return "INCL_NOTE" End If Return area End Function End Class #End Region #Region "Hilfsfunktionen" Private Function FormatCurrency(ByVal pValue As String, pCurrencySymbol As String) As String pValue = pValue.Trim() Dim oBetrag As Decimal Dim culture As Globalization.CultureInfo ' Erkennung des Dezimaltrennzeichens If pValue.Contains(",") AndAlso Not pValue.Contains(".") Then culture = New Globalization.CultureInfo("de-DE") ' Komma → deutsches Format ElseIf pValue.Contains(".") AndAlso Not pValue.Contains(",") Then culture = New Globalization.CultureInfo("en-US") ' Punkt → englisches Format Else ' Mischformat oder Tausendertrennzeichen → Fallback auf aktuelle Culture culture = Globalization.CultureInfo.CurrentCulture End If ' Parsen mit gewählter Culture If Not Decimal.TryParse(pValue, Globalization.NumberStyles.Any, culture, oBetrag) Then Throw New FormatException($"Ungültiger Zahlenwert: {pValue}") End If ' Formatieren mit deutscher Culture Dim oFormatiert As String = oBetrag.ToString("N2", New Globalization.CultureInfo("de-DE")) Return $"{oFormatiert} {pCurrencySymbol}" End Function Private Function FormatStringT(ByVal text As String) As String Return text.Replace(vbCr, " - ").Replace(vbLf, "").Replace(vbTab, " ") End Function Private Function RemoveNewlinesAndTabs(ByVal text As String) As String Return text.Replace(vbCr, " - ").Replace(vbLf, "").Replace(vbTab, " ") End Function Private Function YCoo_TextBoxMinus5(yPosition As Integer) As Integer Return yPosition - 5 End Function Private Function YCoo_TextBoxPlus5(yPosition As Integer) As Integer Return yPosition + 5 End Function Function SplitTextByNewLine(text As String) As List(Of String) If String.IsNullOrEmpty(text) Then Return New List(Of String)() End If ' Zerlege den Text anhand von Zeilenumbrüchen Dim lines As List(Of String) = text.Split({vbCrLf, vbLf, vbCr}, StringSplitOptions.None).ToList() Return lines End Function Private Function Return_InvType(pType As String) As String Dim oReturn As String = "Rechnung/invoice" If pType = "380" Then oReturn = "Handelsrechnung/Commercial invoice" ElseIf pType = "381" Then oReturn = "Gutschriftanzeige/Credit advice" ElseIf pType = "384" Then oReturn = "Rechnungskorrektur/Invoice correction" ElseIf pType = "386" Then oReturn = "Vorauszahlungsrechnung/Prepayment invoice" ElseIf pType = "326" Then oReturn = "Teilrechnung/Partial invoice" ElseIf pType = "84" Then oReturn = "Gutschrift/Credit note" ElseIf pType = "389" Then oReturn = "Gutschriftsverfahren/Credit note procedure" End If Return oReturn End Function Private Function Return_UnitType(pType As String) As String Dim oReturn As String = "Stück/pc" If pType = "C62" Then oReturn = "Stück/pc" ElseIf pType = "DAY" Then oReturn = "Tag/day" ElseIf pType = "HAR" Then oReturn = "Hek/hec" ElseIf pType = "HUR" Then oReturn = "h" ElseIf pType = "KGM" Then oReturn = "kg" ElseIf pType = "KTM" Then oReturn = "km" ElseIf pType = "KWH" Then oReturn = pType ElseIf pType = "LS" Then oReturn = "pausch/flat" ElseIf pType = "MIN" Then oReturn = "minute" ElseIf pType = "MTK" Then oReturn = "QM/SM" ElseIf pType = "Kubik/CM" Then oReturn = "MTR" ElseIf pType = "Meter" Then oReturn = "minute" ElseIf pType = "P1" Then oReturn = "%" ElseIf pType = "SET" Then oReturn = "Set" ElseIf pType = "TNE" Then oReturn = "Tonne/ton" ElseIf pType = "WEE" Then oReturn = "Woche/week" End If Return oReturn End Function #End Region End Class