1235 lines
56 KiB
VB.net
1235 lines
56 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 = 275
|
||
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
|
||
|
||
' Spalten für TAXPOS-Tabelle (Steuerbeträge):
|
||
' BT 116 <TAB> 2.116,36 € (BASEAMOUNT-Zeile)
|
||
' BT 119 <TAB> 7.00 % <TAB> 148,14 € <TAB> VAT (RATE-Zeile)
|
||
Private Const COL_TAXPOS_LABEL As Integer = MARGIN_LEFT ' "BT 116" / "BT 119"
|
||
Private Const COL_TAXPOS_VALUE1 As Integer = 50 ' Basisbetrag ODER Steuersatz (%)
|
||
Private Const COL_TAXPOS_AMOUNT As Integer = 100 ' Steuerbetrag (nur RATE-Zeile)
|
||
Private Const COL_TAXPOS_TYPE As Integer = 150 ' VAT-Kennzeichen (nur RATE-Zeile)
|
||
|
||
' 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 (2.1)")
|
||
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
|
||
' WICHTIG: CurrentPositionPage wird hier NICHT gesetzt.
|
||
' Es gehört zum Positions-Anker und wird nur in HandleArticleTextFollowUp
|
||
' und HandlePositionAmountFollowUp gesetzt – nicht beim Seitenbruch selbst.
|
||
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)
|
||
' ← NEU: Prüfe BEIDE Positionen
|
||
Dim maxY As Integer = Math.Max(context.YPosition, context.YDynamic)
|
||
|
||
If maxY >= 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
|
||
context.YDynamic = context.YPosition ' ← WICHTIG: Reset YDynamic!
|
||
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)
|
||
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
|
||
' ← NEU: VORHER prüfen ob genug Platz für Header + minimale Inhalte (3 Zeilen = 15mm)
|
||
Dim requiredSpace As Integer = 15 ' Header (3x LINE_HEIGHT) + mindestens 1 Inhalt-Zeile
|
||
|
||
If (context.YPosition + requiredSpace) >= PAGE_HEIGHT_LIMIT Then
|
||
CreateNewPage(context)
|
||
_logger.Debug($"Area-Switch [{context.CurrentArea}]: Page break BEFORE header! New YPosition: {context.YPosition}")
|
||
End If
|
||
|
||
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
|
||
_logger.Debug($"Drawing area header for [{context.CurrentArea}] with caption [{caption}] at YPosition={context.YPosition}")
|
||
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.CurrentPositionStartY = context.YPosition
|
||
context.CurrentPositionPage = context.PDF.GetPageCount()
|
||
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)
|
||
' Die TAXPOS-Area kann pro Steuersatz-Gruppe mit BASEAMOUNT ODER RATE beginnen:
|
||
' Gruppe: [BASEAMOUNT] RATE AMOUNT TYPE (BASEAMOUNT optional, ggf. nicht vorhanden)
|
||
' Jede Gruppe ergibt GENAU EINE Zeile, sobald TYPE (VAT) eintrifft - außer der
|
||
' optionalen BASEAMOUNT-Zeile, die SOFORT und SEPARAT gerendert wird.
|
||
' WICHTIG: Dies ist die allererste Zeile der Area - kein zusätzlicher Zeilenvorschub.
|
||
context.IsFirstTaxPosDisplay = True
|
||
|
||
If itemData.SpecName = "INVOICE_TAXPOS_BASEAMOUNT" Then
|
||
RenderTaxposBaseAmountRow(context, itemData)
|
||
itemData.Display = False
|
||
ElseIf itemData.SpecName = "INVOICE_TAXPOS_RATE" Then
|
||
context.TaxPosRate = itemData.Value
|
||
context.TaxPosRateCaption = itemData.Caption
|
||
itemData.Display = False
|
||
_logger.Debug($"TAXPOS RATE in AreaSwitch (group start, no BASEAMOUNT) accumulated: [{context.TaxPosRate}]")
|
||
End If
|
||
End Sub
|
||
|
||
''' <summary>
|
||
''' Rendert die Basisbetrag-Zeile sofort in eigener Zeile: "BT 116 <TAB> 2.116,36 €"
|
||
''' Diese Zeile ist unabhängig von der nachfolgenden RATE/AMOUNT/TYPE-Zeile.
|
||
''' </summary>
|
||
Private Sub RenderTaxposBaseAmountRow(context As PdfRenderContext, itemData As InvoiceItemData)
|
||
' Erste Zeile der TAXPOS-Area braucht keinen zusätzlichen Zeilenvorschub
|
||
' (Area-Header hat YPosition bereits korrekt positioniert); alle weiteren Zeilen schon.
|
||
If Not context.IsFirstTaxPosDisplay Then
|
||
context.YPosition += LINE_HEIGHT
|
||
End If
|
||
context.IsFirstTaxPosDisplay = False
|
||
|
||
Dim baseAmountFormatted As String = FormatCurrency(itemData.Value, context.CurrencySymbol)
|
||
context.PDF.DrawText(fontResName, COL_TAXPOS_LABEL, context.YPosition, itemData.Caption)
|
||
context.PDF.DrawText(fontResName, COL_TAXPOS_VALUE1, context.YPosition, baseAmountFormatted)
|
||
_logger.Debug($"TAXPOS BASEAMOUNT row rendered: [{itemData.Caption}] [{baseAmountFormatted}] at Y={context.YPosition}")
|
||
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
|
||
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
|
||
HandleTaxRateFollowUp(context, itemData)
|
||
ElseIf {"RECEIPT_ALLOWANCE_VAT_RATE", "POSITION_ALLOWANCE_VAT_RATE"}.Contains(itemData.SpecName) Then
|
||
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
|
||
itemData.Display = False
|
||
ElseIf {"RECEIPT_ALLOWANCE_CALCULATION_PERCENT", "POSITION_ALLOWANCE_CALCULATION_PERCENT"}.Contains(itemData.SpecName) Then
|
||
itemData.Display = False
|
||
ElseIf itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then
|
||
itemData.Display = False
|
||
End If
|
||
End Sub
|
||
|
||
Private Sub HandlePositionAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean)
|
||
context.PositionCount += 1
|
||
|
||
' *** BUGFIX: Unnötige Leerzeile zwischen Positionen ***
|
||
'
|
||
' HandleArticleTextFollowUp synct am Ende jeder Position:
|
||
' context.YPosition = Math.Max(context.YPosition, context.YDynamic)
|
||
' Nach einer mehrzeiligen DESCRIPTION (z.B. 2 Teile) zeigt YDynamic bereits auf die
|
||
' NÄCHSTE freie Zeile (RenderMultiLineText erhöht YDynamic nach jedem Teil). D.h.
|
||
' YPosition steht zu Beginn dieser Methode oft schon korrekt auf der nächsten freien
|
||
' Zeile - ein zusätzliches "+= LINE_HEIGHT" weiter unten würde dann eine echte
|
||
' Leerzeile zwischen letzter DESCRIPTION-Zeile und der neuen Position erzeugen.
|
||
'
|
||
' Erkennungsmerkmal: YDynamic > 0 UND YDynamic = YPosition bedeutet, die vorherige
|
||
' Position hat bereits über RenderMultiLineText/HandleArticleTextFollowUp die
|
||
' YPosition auf die korrekte nächste freie Zeile gesetzt - dann KEIN weiterer Vorschub.
|
||
Dim yPositionAlreadyAdvanced As Boolean = (context.YDynamic > 0) AndAlso (context.YDynamic = context.YPosition)
|
||
|
||
If Not descriptionFollowup Then
|
||
context.YPlus = 0
|
||
context.YDynamic = 0
|
||
End If
|
||
|
||
If Not yPositionAlreadyAdvanced Then
|
||
context.YPosition += LINE_HEIGHT
|
||
End If
|
||
context.CurrentPositionStartY = context.YPosition
|
||
context.CurrentPositionPage = context.PDF.GetPageCount()
|
||
|
||
' ✓ OPTIMIERT: Von 11mm auf 6mm
|
||
' WARUM 6mm?
|
||
' - Position#-Zeile: 5mm (wird SOFORT gerendert)
|
||
' - Tax/Amount: 0mm Extra (werden auf DERSELBEN Y-Position gerendert via CurrentPositionStartY)
|
||
' - Minimaler Puffer: 1mm (für Textbox-Rendering-Toleranzen)
|
||
Dim requiredSpace As Integer = 6
|
||
|
||
If (context.YPosition + requiredSpace) >= PAGE_HEIGHT_LIMIT Then
|
||
CreateNewPage(context)
|
||
context.YDynamic = context.YPosition
|
||
context.CurrentPositionStartY = context.YPosition
|
||
context.CurrentPositionPage = context.PDF.GetPageCount() ' neue Seite ist der Anker
|
||
_logger.Debug($"HandlePositionAmountFollowUp: Page break! New YPosition={context.YPosition}, CurrentPositionStartY={context.CurrentPositionStartY}, Page={context.CurrentPositionPage}")
|
||
End If
|
||
|
||
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
|
||
|
||
' *** BUGFIX: CurrentPositionStartY auf die ERSTE Zeile von INVOICE_POSITION_ARTICLE verankern ***
|
||
'
|
||
' Tax (COL_POS_TAX) und Amount (COL_POS_SUM) müssen in der ERSTEN Zeile des Artikeltexts
|
||
' erscheinen - dort wo der Artikelname und damit auch der Steuersatz/Betrag laut Layout
|
||
' hingehören. Bei kurzen (einzeiligen) Artikeln ist "erste Zeile" = "letzte Zeile", daher
|
||
' lieferte die alte Berechnung (YDynamic - LINE_HEIGHT, NACH dem Rendern) bislang dasselbe
|
||
' Ergebnis. Bei LANGEN, mehrzeiligen Artikeltexten (z.B. 15 Zeilen Prüfbeschreibung, die
|
||
' selbst über einen Seitenbruch laufen) zeigte "YDynamic - LINE_HEIGHT NACH dem Rendern"
|
||
' auf die LETZTE Zeile des Artikeltexts (ggf. auf einer späteren Seite) - das war falsch.
|
||
'
|
||
' Die neuen ByRef-Parameter firstAnchorY/firstAnchorPage liefern stattdessen exakt den
|
||
' Y-Wert und die Seite des ALLERERSTEN tatsächlich gezeichneten Textteils, erfasst
|
||
' INNERHALB von RenderMultiLineText direkt vor dem ersten DrawText-Aufruf - das ist in
|
||
' allen Fällen korrekt, auch wenn der Seitenbruch noch VOR dem ersten Zeichnen eintritt
|
||
' (Szenario B: Artikel beginnt komplett auf neuer Seite).
|
||
'
|
||
' Szenario A – kein Seitenbruch, kurzer Artikel: erste Zeile=240/Seite1 → Anker=240/1 (wie bisher) ✓
|
||
' Szenario B – FIRST-part-Break (Artikel beginnt auf neuer Seite): erste Zeile=45/Seite2 → Anker=45/2 (wie bisher) ✓
|
||
' Szenario C – CONTINUATION-Break in DESCRIPTION (Artikel einzeilig, bleibt auf alter Seite):
|
||
' erste Zeile=265/Seite1 → Anker=265/1 (wie bisher, unverändert) ✓
|
||
' Szenario D – NEU: langer mehrzeiliger ARTIKEL, der selbst über mehrere Seiten läuft:
|
||
' erste Zeile=220/Seite1 → Anker=220/1 (KORRIGIERT, vorher fälschlich letzte Zeile/spätere Seite)
|
||
If itemData.SpecName = "INVOICE_POSITION_ARTICLE" Then
|
||
Dim firstAnchorY As Integer = -1
|
||
Dim firstAnchorPage As Integer = -1
|
||
RenderMultiLineText(context, itemData.Value, xPos, MAX_TEXT_LENGTH_POSITION, firstAnchorY, firstAnchorPage)
|
||
|
||
context.CurrentPositionStartY = firstAnchorY
|
||
context.CurrentPositionPage = firstAnchorPage
|
||
_logger.Debug($"HandleArticleTextFollowUp: CurrentPositionStartY anchored to FIRST ARTICLE row Y={context.CurrentPositionStartY} on page {context.CurrentPositionPage}")
|
||
Else
|
||
RenderMultiLineText(context, itemData.Value, xPos, MAX_TEXT_LENGTH_POSITION)
|
||
End If
|
||
|
||
' YPosition immer synchronisieren
|
||
context.YPosition = Math.Max(context.YPosition, context.YDynamic)
|
||
|
||
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)
|
||
|
||
' ✓ KORRIGIERT: YPosition synchronisieren
|
||
context.YPosition = Math.Max(context.YPosition, context.YDynamic)
|
||
|
||
itemData.Display = False
|
||
End Sub
|
||
|
||
Private Sub HandleTaxRateFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
|
||
Dim yPos As Integer = context.CurrentPositionStartY
|
||
Dim targetPage As Integer = context.CurrentPositionPage
|
||
Dim activePage As Integer = context.PDF.GetPageCount()
|
||
|
||
_logger.Debug($"Handling Tax Rate Follow-Up: Value=[{itemData.Value}] at YPos={yPos}, targetPage={targetPage}, activePage={activePage}")
|
||
|
||
If yPos < MARGIN_TOP + 30 Then
|
||
_logger.Warn($"TaxRate: yPos={yPos} ungültig. Verwende YPosition={context.YPosition}.")
|
||
yPos = context.YPosition
|
||
targetPage = activePage
|
||
End If
|
||
|
||
If targetPage <> activePage Then
|
||
context.PDF.SelectPage(targetPage)
|
||
_logger.Debug($"TaxRate: SelectPage({targetPage}) to draw at Y={yPos}, then back to page {activePage}")
|
||
End If
|
||
|
||
' SetTextSize explizit setzen: GdPicturePDF hält Font-Zustand global;
|
||
' nach SelectPage ist er undefiniert und muss vor jedem DrawText normalisiert werden.
|
||
context.PDF.SetTextSize(TEXT_SIZE_NORMAL)
|
||
context.PDF.DrawText(fontResName, COL_POS_TAX, yPos, $"{itemData.Value} %")
|
||
|
||
If targetPage <> activePage Then
|
||
context.PDF.SelectPage(activePage)
|
||
context.PDF.SetTextSize(TEXT_SIZE_NORMAL) ' Font-Zustand der aktiven Seite wiederherstellen
|
||
End If
|
||
|
||
itemData.Display = False
|
||
End Sub
|
||
|
||
Private Sub HandlePositionTaxAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
|
||
Dim yPos As Integer = context.CurrentPositionStartY
|
||
Dim targetPage As Integer = context.CurrentPositionPage
|
||
Dim activePage As Integer = context.PDF.GetPageCount()
|
||
|
||
If yPos < MARGIN_TOP + 30 Then
|
||
_logger.Warn($"TaxAmount: yPos={yPos} ungültig. Verwende YPosition={context.YPosition}.")
|
||
yPos = context.YPosition
|
||
targetPage = activePage
|
||
End If
|
||
|
||
Dim yPosAdjusted As Double = yPos - 3.5
|
||
Dim taxTerm As String = FormatCurrency(itemData.Value, context.CurrencySymbol)
|
||
_logger.Debug($"Handling Position Tax Amount Follow-Up: Value=[{itemData.Value}] Formatted=[{taxTerm}] at YPos={yPos}, Adjusted={yPosAdjusted}, targetPage={targetPage}, activePage={activePage}")
|
||
|
||
If targetPage <> activePage Then
|
||
context.PDF.SelectPage(targetPage)
|
||
_logger.Debug($"TaxAmount: SelectPage({targetPage}) to draw at Y={yPosAdjusted}, then back to page {activePage}")
|
||
End If
|
||
|
||
' SetTextSize explizit setzen – gleicher Grund wie in HandleTaxRateFollowUp.
|
||
context.PDF.SetTextSize(TEXT_SIZE_NORMAL)
|
||
context.PDF.DrawTextBox(fontResName, 177, yPosAdjusted, 198, YCoo_TextBoxPlus5(yPosAdjusted),
|
||
TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentNear,
|
||
taxTerm)
|
||
|
||
If targetPage <> activePage Then
|
||
context.PDF.SelectPage(activePage)
|
||
context.PDF.SetTextSize(TEXT_SIZE_NORMAL) ' Font-Zustand der aktiven Seite wiederherstellen
|
||
End If
|
||
|
||
itemData.Display = False
|
||
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)
|
||
If itemData.SpecName = "INVOICE_TAXPOS_BASEAMOUNT" Then
|
||
' BASEAMOUNT kann auch als Follow-Up (zweite und weitere Steuersatz-Gruppen)
|
||
' auftreten, nicht nur im allerersten Area-Switch. Eigene Zeile, sofort gerendert.
|
||
RenderTaxposBaseAmountRow(context, itemData)
|
||
itemData.Display = False
|
||
ElseIf itemData.SpecName = "INVOICE_TAXPOS_RATE" Then
|
||
context.PositionCount += 1
|
||
context.TaxPosRate = itemData.Value
|
||
context.TaxPosRateCaption = itemData.Caption
|
||
itemData.Display = False
|
||
_logger.Debug($"TAXPOS RATE accumulated (new group): [{context.TaxPosRate}]")
|
||
ElseIf itemData.SpecName = "INVOICE_TAXPOS_AMOUNT" Then
|
||
context.TaxPosAmount = FormatCurrency(itemData.Value, context.CurrencySymbol)
|
||
itemData.Display = False
|
||
_logger.Debug($"TAXPOS AMOUNT accumulated: [{context.TaxPosAmount}]")
|
||
ElseIf itemData.SpecName = "INVOICE_TAXPOS_TYPE" Then
|
||
' Vollständige Gruppe (RATE + AMOUNT + TYPE) ist da → eine Zeile rendern:
|
||
' "BT 119 <TAB> 7.00 % <TAB> 148,14 € <TAB> VAT"
|
||
If Not String.IsNullOrEmpty(context.TaxPosRate) Then
|
||
' Nur Zeilenvorschub wenn dies NICHT die allererste Zeile der Area ist
|
||
' (z.B. wenn die Gruppe direkt mit RATE begann, ohne BASEAMOUNT davor).
|
||
If Not context.IsFirstTaxPosDisplay Then
|
||
context.YPosition += LINE_HEIGHT
|
||
End If
|
||
context.IsFirstTaxPosDisplay = False
|
||
|
||
context.PDF.DrawText(fontResName, COL_TAXPOS_LABEL, context.YPosition, context.TaxPosRateCaption)
|
||
context.PDF.DrawText(fontResName, COL_TAXPOS_VALUE1, context.YPosition, $"{context.TaxPosRate} %")
|
||
context.PDF.DrawText(fontResName, COL_TAXPOS_AMOUNT, context.YPosition, context.TaxPosAmount)
|
||
context.PDF.DrawText(fontResName, COL_TAXPOS_TYPE, context.YPosition, itemData.Value)
|
||
|
||
_logger.Debug($"TAXPOS row rendered: [{context.TaxPosRateCaption}] [{context.TaxPosRate} %] [{context.TaxPosAmount}] [{itemData.Value}] at Y={context.YPosition}")
|
||
|
||
' Reset für nächste TAXPOS-Gruppe
|
||
context.TaxPosRate = ""
|
||
context.TaxPosRateCaption = ""
|
||
context.TaxPosAmount = ""
|
||
Else
|
||
_logger.Debug($"TAXPOS TYPE DUPLICATE/incomplete ignored: [{itemData.Value}]")
|
||
End If
|
||
itemData.Display = False
|
||
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,
|
||
Optional ByRef firstRenderedY As Integer = -1,
|
||
Optional ByRef firstRenderedPage As Integer = -1)
|
||
Dim partsNL As List(Of String) = StringFunctions.SplitTextByNewLine(text)
|
||
Dim isFirstPart As Boolean = True
|
||
|
||
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
|
||
' ✓ PROBLEM GEFUNDEN: requiredSpace=10 ist zu viel!
|
||
' Bei YDynamic=230 prüft es: 230 + 10 = 240 >= 270 → TRUE → Seitenumbruch
|
||
' ABER: Die Zeile selbst benötigt nur 5mm! 230 + 5 = 235 wäre OK!
|
||
|
||
Dim requiredSpace As Integer = 6 ' ← REDUZIERT: Nur 1mm Puffer (Zeile=5mm + 1mm)
|
||
|
||
If (context.YDynamic + requiredSpace) >= PAGE_HEIGHT_LIMIT Then
|
||
' Neue Seite erstellen
|
||
CreateNewPage(context)
|
||
context.YDynamic = context.YPosition
|
||
|
||
' WICHTIG: CurrentPositionStartY wird hier NICHT geändert.
|
||
' Der korrekte Anker ist die Zeile von INVOICE_POSITION_ARTICLE,
|
||
' gesetzt in HandleArticleTextFollowUp VOR dem RenderMultiLineText-Aufruf.
|
||
' - CONTINUATION-Break: Artikel auf alter Seite → CurrentPositionStartY
|
||
' bleibt auf der alten Seite (Tax/Amount landen korrekt neben Artikel).
|
||
' - FIRST-part-Break im ARTICLE-Feld: HandleArticleTextFollowUp hat
|
||
' CurrentPositionStartY = context.YDynamic gesetzt, BEVOR CreateNewPage
|
||
' aufgerufen wurde → nach dem Seitenbruch zeigt YDynamic auf die neue
|
||
' Seite und der Anker ist bereits korrekt (45).
|
||
If isFirstPart Then
|
||
_logger.Debug($"RenderMultiLineText: Page break at FIRST part! YDynamic={context.YDynamic}, CurrentPositionStartY unchanged={context.CurrentPositionStartY}")
|
||
Else
|
||
_logger.Debug($"RenderMultiLineText: Page break at continuation! YDynamic={context.YDynamic}, CurrentPositionStartY unchanged={context.CurrentPositionStartY}")
|
||
End If
|
||
End If
|
||
|
||
' *** BUGFIX (lange mehrzeilige ARTICLE-Texte über Seitenbruch hinweg) ***
|
||
' Erfasst Y/Seite des ALLERERSTEN tatsächlich gezeichneten Textteils - das ist
|
||
' immer die korrekte Ankerzeile, unabhängig davon, ob danach noch weitere Zeilen
|
||
' folgen oder ein Seitenbruch mitten im Text auftritt. Wird nur befüllt, wenn der
|
||
' Aufrufer (HandleArticleTextFollowUp) die Parameter explizit übergibt.
|
||
If firstRenderedY = -1 Then
|
||
firstRenderedY = context.YDynamic
|
||
firstRenderedPage = context.PDF.GetPageCount()
|
||
End If
|
||
|
||
_logger.Debug($"RenderMultiLineText: Rendering part: [{part}] at Y position: {context.YDynamic}")
|
||
context.PDF.DrawText(fontResName, xPos, context.YDynamic, part)
|
||
context.YDynamic += LINE_HEIGHT
|
||
context.YPlus += LINE_HEIGHT
|
||
isFirstPart = False
|
||
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 ' Legacy, nicht mehr aktiv genutzt - bleibt für Kompatibilität
|
||
Public Property TaxPosRate As String
|
||
Public Property TaxPosRateCaption As String
|
||
Public Property TaxPosAmount As String
|
||
Public Property IsFirstTaxPosDisplay As Boolean
|
||
Public Property CurrentPositionStartY As Integer
|
||
' Seitennummer auf der CurrentPositionStartY gültig ist.
|
||
' Wird beim Setzen von CurrentPositionStartY immer mitgesetzt.
|
||
Public Property CurrentPositionPage As Integer
|
||
|
||
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 = ""
|
||
TaxPosRate = ""
|
||
TaxPosRateCaption = ""
|
||
TaxPosAmount = ""
|
||
IsFirstTaxPosDisplay = False
|
||
CurrentPositionStartY = 0
|
||
CurrentPositionPage = 1
|
||
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 |