Files
Modules/Jobs/ZUGFeRD/XRechnungViewDocument.vb
Developer01 85bbceae0a Modularisierung und Optimierung der PDF-Erstellung
Die Methode `Create_PDFfromXML` wurde vollständig überarbeitet, um die Struktur und Lesbarkeit zu verbessern. Die Logik wurde modularisiert, indem neue Methoden wie `InitializeFilePaths`, `InitializePDF`, `ProcessInvoiceData` und `FinalizePDF` eingeführt wurden.

Neue Hilfsklassen (`FilePaths`, `PdfRenderContext`, `InvoiceItemData`) wurden hinzugefügt, um die Datenstrukturierung und den Kontext zu verbessern. Die Verarbeitung von Bereichen und Folgeelementen wurde in spezifische Methoden ausgelagert (`HandleAreaSwitch`, `HandleFollowUpItem`).

Die Rendering-Logik wurde durch Methoden wie `RenderDisplayItem` und `RenderMultiLineText` vereinfacht. Neue Konstanten für Layout und Textformate wurden eingeführt, um die Standardisierung zu fördern.

Die Debug-Logs wurden erweitert, um detaillierte Einblicke in die Verarbeitungsschritte zu bieten. Die Änderungen verbessern die Wartbarkeit, Modularität und Robustheit der PDF-Erstellung erheblich.
2026-06-15 10:03:10 +02:00

983 lines
42 KiB
VB.net

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