2 Commits

Author SHA1 Message Date
Developer01
9d738a9288 Merge branch 'master' of https://vcs.digitaldata.works/AppStd/Modules 2026-06-30 10:29:14 +02:00
Developer01
b6d3e61488 Messaging: OAuth Sending
Jobs: Sichtbeleg Anpassungen, Seitenwechsel, Freiräume und Taxpos
2026-06-30 10:29:00 +02:00
7 changed files with 2716 additions and 543 deletions

View File

@@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices
<Assembly: AssemblyCompany("Digital Data")> <Assembly: AssemblyCompany("Digital Data")>
<Assembly: AssemblyProduct("Modules.Jobs")> <Assembly: AssemblyProduct("Modules.Jobs")>
<Assembly: AssemblyCopyright("Copyright © 2026")> <Assembly: AssemblyCopyright("Copyright © 2026")>
<Assembly: AssemblyTrademark("3.7.0")> <Assembly: AssemblyTrademark("3.8.0")>
<Assembly: ComVisible(False)> <Assembly: ComVisible(False)>
@@ -30,5 +30,5 @@ Imports System.Runtime.InteropServices
' Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern ' Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern
' übernehmen, indem Sie "*" eingeben: ' übernehmen, indem Sie "*" eingeben:
<Assembly: AssemblyVersion("3.7.0.0")> <Assembly: AssemblyVersion("3.8.0.0")>
<Assembly: AssemblyFileVersion("3.7.0.0")> <Assembly: AssemblyFileVersion("3.8.0.0")>

View File

@@ -24,7 +24,7 @@ Public Class XRechnungViewDocument
Private Const MARGIN_LEFT As Integer = 10 Private Const MARGIN_LEFT As Integer = 10
Private Const MARGIN_TOP As Integer = 15 Private Const MARGIN_TOP As Integer = 15
Private Const LINE_WIDTH As Integer = 200 Private Const LINE_WIDTH As Integer = 200
Private Const PAGE_HEIGHT_LIMIT As Integer = 270 Private Const PAGE_HEIGHT_LIMIT As Integer = 275
Private Const FOOTER_Y As Integer = 280 Private Const FOOTER_Y As Integer = 280
Private Const FOOTER_TEXT_Y As Integer = 285 Private Const FOOTER_TEXT_Y As Integer = 285
Private Const LINE_HEIGHT As Integer = 5 Private Const LINE_HEIGHT As Integer = 5
@@ -39,6 +39,14 @@ Public Class XRechnungViewDocument
Private Const COL_POS_SUM As Integer = 181 Private Const COL_POS_SUM As Integer = 181
Private Const COL_VALUE_X As Integer = 70 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 ' Text-Größen
Private Const TEXT_SIZE_TITLE As Integer = 18 Private Const TEXT_SIZE_TITLE As Integer = 18
Private Const TEXT_SIZE_NORMAL As Integer = 10 Private Const TEXT_SIZE_NORMAL As Integer = 10
@@ -174,6 +182,9 @@ Public Class XRechnungViewDocument
DrawHeader(context.PDF) DrawHeader(context.PDF)
DrawFooter(context.PDF, context.CreatedString) DrawFooter(context.PDF, context.CreatedString)
context.YPosition = MARGIN_TOP + 30 ' Nach Header 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 End Sub
Private Sub DrawHeader(pdf As GdPicturePDF) Private Sub DrawHeader(pdf As GdPicturePDF)
@@ -412,6 +423,7 @@ Public Class XRechnungViewDocument
context.YDynamic = 0 context.YDynamic = 0
context.YPosition += LINE_HEIGHT ' Neue Zeile für erste Position context.YPosition += LINE_HEIGHT ' Neue Zeile für erste Position
context.CurrentPositionStartY = context.YPosition 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_NUMBER, context.YPosition, "1") ' Erste Position ist immer 1
context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, itemData.Value) context.PDF.DrawText(fontResName, COL_POS_AMOUNT, context.YPosition, itemData.Value)
context.PositionCount = 1 ' Zähler auf 1 setzen statt increment context.PositionCount = 1 ' Zähler auf 1 setzen statt increment
@@ -447,15 +459,42 @@ Public Class XRechnungViewDocument
End Sub End Sub
Private Sub HandleTaxPosAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData) Private Sub HandleTaxPosAreaSwitch(context As PdfRenderContext, itemData As InvoiceItemData)
If itemData.SpecName = "INVOICE_TAXPOS_RATE" Then ' Die TAXPOS-Area kann pro Steuersatz-Gruppe mit BASEAMOUNT ODER RATE beginnen:
context.PositionCount = 1 ' Gruppe: [BASEAMOUNT] RATE AMOUNT TYPE (BASEAMOUNT optional, ggf. nicht vorhanden)
context.TaxPosText = $"{itemData.Value} %: " ' ← In Context speichern! ' Jede Gruppe ergibt GENAU EINE Zeile, sobald TYPE (VAT) eintrifft - außer der
context.IsFirstTaxPosDisplay = True ' ← Flag setzen! ' 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 itemData.Display = False
_logger.Debug($"TAXPOS RATE in AreaSwitch accumulated: [{context.TaxPosText}]") 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 If
End Sub 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 #End Region
@@ -506,7 +545,6 @@ Public Class XRechnungViewDocument
ElseIf itemData.SpecName = "INVOICE_POSITION_UNIT_TYPE" Then ElseIf itemData.SpecName = "INVOICE_POSITION_UNIT_TYPE" Then
HandleUnitTypeFollowUp(context, itemData) HandleUnitTypeFollowUp(context, itemData)
ElseIf {"POSITION_ALLOWANCE_REASON", "RECEIPT_ALLOWANCE_REASON"}.Contains(itemData.SpecName) Then 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) context.PDF.DrawText(fontResName, COL_POS_REASON, context.YPosition, itemData.Value)
itemData.Display = False itemData.Display = False
ElseIf {"INVOICE_POSITION_ARTICLE", "INVOICE_POSITION_ARTICLE_DESCRIPTION"}.Contains(itemData.SpecName) Then ElseIf {"INVOICE_POSITION_ARTICLE", "INVOICE_POSITION_ARTICLE_DESCRIPTION"}.Contains(itemData.SpecName) Then
@@ -514,37 +552,63 @@ Public Class XRechnungViewDocument
ElseIf itemData.SpecName = "INVOICE_POSITION_NOTE" Then ElseIf itemData.SpecName = "INVOICE_POSITION_NOTE" Then
HandlePositionNoteFollowUp(context, itemData) HandlePositionNoteFollowUp(context, itemData)
ElseIf {"INVOICE_TAXPOS_TAX_RATE", "INVOICE_TAXPOS_RATE"}.Contains(itemData.SpecName) Then ElseIf {"INVOICE_TAXPOS_TAX_RATE", "INVOICE_TAXPOS_RATE"}.Contains(itemData.SpecName) Then
' ← NUR für POSITION: Tax Rate
HandleTaxRateFollowUp(context, itemData) HandleTaxRateFollowUp(context, itemData)
ElseIf {"RECEIPT_ALLOWANCE_VAT_RATE", "POSITION_ALLOWANCE_VAT_RATE"}.Contains(itemData.SpecName) Then 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) HandleTaxRateFollowUp(context, itemData)
ElseIf itemData.SpecName = "INVOICE_POSITION_TAX_AMOUNT" Then ElseIf itemData.SpecName = "INVOICE_POSITION_TAX_AMOUNT" Then
HandlePositionTaxAmountFollowUp(context, itemData) HandlePositionTaxAmountFollowUp(context, itemData)
ElseIf {"RECEIPT_ALLOWANCE_VAT_CODE", "POSITION_ALLOWANCE_VAT_CODE"}.Contains(itemData.SpecName) Then ElseIf {"RECEIPT_ALLOWANCE_VAT_CODE", "POSITION_ALLOWANCE_VAT_CODE"}.Contains(itemData.SpecName) Then
' VAT_CODE wird nicht angezeigt (nur Metadata)
itemData.Display = False itemData.Display = False
ElseIf {"RECEIPT_ALLOWANCE_CALCULATION_PERCENT", "POSITION_ALLOWANCE_CALCULATION_PERCENT"}.Contains(itemData.SpecName) Then ElseIf {"RECEIPT_ALLOWANCE_CALCULATION_PERCENT", "POSITION_ALLOWANCE_CALCULATION_PERCENT"}.Contains(itemData.SpecName) Then
' ← NEU: CALCULATION_PERCENT wird nicht angezeigt (nur Metadata)
itemData.Display = False itemData.Display = False
ElseIf itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then ElseIf itemData.SpecName = "RECEIPT_ALLOWANCE_CHARGE_INDICATOR" Then
' CHARGE_INDICATOR im Follow-Up (zweite Allowance) wird nicht angezeigt
itemData.Display = False itemData.Display = False
End If End If
End Sub End Sub
Private Sub HandlePositionAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean) Private Sub HandlePositionAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData, descriptionFollowup As Boolean)
context.PositionCount += 1 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 If Not descriptionFollowup Then
context.YPlus = 0 context.YPlus = 0
context.YDynamic = 0 context.YDynamic = 0
End If End If
' WICHTIG: Neue Zeile für jede neue Position! If Not yPositionAlreadyAdvanced Then
context.YPosition += LINE_HEIGHT context.YPosition += LINE_HEIGHT
End If
' ← NEU: Start-Y der aktuellen Position speichern
context.CurrentPositionStartY = context.YPosition 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()) context.PDF.DrawText(fontResName, COL_POS_NUMBER, context.YPosition, context.PositionCount.ToString())
@@ -587,9 +651,41 @@ Public Class XRechnungViewDocument
xPos = COL_POS_REASON xPos = COL_POS_REASON
End If End If
RenderMultiLineText(context, itemData.Value, xPos, MAX_TEXT_LENGTH_POSITION) ' *** 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)
' ← NEU: YPosition auf den höchsten erreichten Wert setzen 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) context.YPosition = Math.Max(context.YPosition, context.YDynamic)
itemData.Display = False itemData.Display = False
@@ -602,28 +698,74 @@ Public Class XRechnungViewDocument
RenderMultiLineText(context, itemData.Value, COL_POS_TEXT, MAX_TEXT_LENGTH_NOTE) RenderMultiLineText(context, itemData.Value, COL_POS_TEXT, MAX_TEXT_LENGTH_NOTE)
' ← NEU: YPosition synchronisieren ' ✓ KORRIGIERT: YPosition synchronisieren
context.YPosition = Math.Max(context.YPosition, context.YDynamic) context.YPosition = Math.Max(context.YPosition, context.YDynamic)
itemData.Display = False itemData.Display = False
End Sub End Sub
Private Sub HandleTaxRateFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) Private Sub HandleTaxRateFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
' ← VERWENDE die Start-Y-Position der aktuellen Position!
Dim yPos As Integer = context.CurrentPositionStartY Dim yPos As Integer = context.CurrentPositionStartY
_logger.Debug($"Handling Tax Rate Follow-Up: Value=[{itemData.Value}] at YPos={yPos}") 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} %") 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 itemData.Display = False
End Sub End Sub
Private Sub HandlePositionTaxAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) Private Sub HandlePositionTaxAmountFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
Dim yPos As Integer = context.CurrentPositionStartY 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 yPosAdjusted As Double = yPos - 3.5
Dim taxTerm As String = FormatCurrency(itemData.Value, context.CurrencySymbol) 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 YPos={yPosAdjusted}") _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), context.PDF.DrawTextBox(fontResName, 177, yPosAdjusted, 198, YCoo_TextBoxPlus5(yPosAdjusted),
TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentNear, TextAlignment.TextAlignmentFar, TextAlignment.TextAlignmentNear,
taxTerm) 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 itemData.Display = False
End Sub End Sub
@@ -634,23 +776,47 @@ Public Class XRechnungViewDocument
End Sub End Sub
Private Sub HandleTaxPosFollowUp(context As PdfRenderContext, itemData As InvoiceItemData) Private Sub HandleTaxPosFollowUp(context As PdfRenderContext, itemData As InvoiceItemData)
' TAXPOS Items werden zu einem String kombiniert If itemData.SpecName = "INVOICE_TAXPOS_BASEAMOUNT" Then
If itemData.SpecName = "INVOICE_TAXPOS_RATE" 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.PositionCount += 1
context.TaxPosText = $"{itemData.Value} %: " ' Speichern statt direkt setzen context.TaxPosRate = itemData.Value
context.TaxPosRateCaption = itemData.Caption
itemData.Display = False itemData.Display = False
_logger.Debug($"TAXPOS RATE accumulated: [{context.TaxPosText}]") _logger.Debug($"TAXPOS RATE accumulated (new group): [{context.TaxPosRate}]")
ElseIf itemData.SpecName = "INVOICE_TAXPOS_AMOUNT" Then ElseIf itemData.SpecName = "INVOICE_TAXPOS_AMOUNT" Then
Dim amount As String = FormatCurrency(itemData.Value, context.CurrencySymbol) context.TaxPosAmount = FormatCurrency(itemData.Value, context.CurrencySymbol)
context.TaxPosText &= amount ' Anhängen
itemData.Display = False itemData.Display = False
_logger.Debug($"TAXPOS AMOUNT accumulated: [{context.TaxPosText}]") _logger.Debug($"TAXPOS AMOUNT accumulated: [{context.TaxPosAmount}]")
ElseIf itemData.SpecName = "INVOICE_TAXPOS_TYPE" Then ElseIf itemData.SpecName = "INVOICE_TAXPOS_TYPE" Then
context.TaxPosText &= $" {itemData.Value}" ' Anhängen ' Vollständige Gruppe (RATE + AMOUNT + TYPE) ist da → eine Zeile rendern:
itemData.Value = context.TaxPosText ' JETZT den kombinierten String setzen ' "BT 119 <TAB> 7.00 % <TAB> 148,14 € <TAB> VAT"
itemData.Display = True ' Und anzeigen! If Not String.IsNullOrEmpty(context.TaxPosRate) Then
context.TaxPosText = "" ' Reset für nächste TAXPOS ' Nur Zeilenvorschub wenn dies NICHT die allererste Zeile der Area ist
_logger.Debug($"TAXPOS TYPE final: [{itemData.Value}], Display=True") ' (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 ElseIf itemData.Value.Contains("EXEMPTION") Then
_logger.Debug($"We got an Exemption: {itemData.Value}") _logger.Debug($"We got an Exemption: {itemData.Value}")
End If End If
@@ -731,28 +897,58 @@ Public Class XRechnungViewDocument
End If End If
End Sub End Sub
Private Sub RenderMultiLineText(context As PdfRenderContext, text As String, xPos As Integer, maxLength As Integer) Private Sub RenderMultiLineText(context As PdfRenderContext, text As String, xPos As Integer, maxLength As Integer,
' ERST nach echten Zeilenumbrüchen aufteilen Optional ByRef firstRenderedY As Integer = -1,
Optional ByRef firstRenderedPage As Integer = -1)
Dim partsNL As List(Of String) = StringFunctions.SplitTextByNewLine(text) Dim partsNL As List(Of String) = StringFunctions.SplitTextByNewLine(text)
Dim isFirstPart As Boolean = True
For Each linePart As String In partsNL For Each linePart As String In partsNL
' DANN jede Zeile bei maxLength umbrechen
Dim parts As List(Of String) = StringFunctions.SplitText_Length(linePart, maxLength) Dim parts As List(Of String) = StringFunctions.SplitText_Length(linePart, maxLength)
For Each part As String In parts For Each part As String In parts
' ← NEU: VOR jedem Rendering prüfen, ob neue Seite nötig! ' ✓ PROBLEM GEFUNDEN: requiredSpace=10 ist zu viel!
If context.YDynamic >= PAGE_HEIGHT_LIMIT Then ' 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 ' Neue Seite erstellen
CreateNewPage(context) CreateNewPage(context)
' YDynamic auf neue Startposition setzen (nach Header)
context.YDynamic = context.YPosition context.YDynamic = context.YPosition
_logger.Debug($"RenderMultiLineText: Page break! New YDynamic: {context.YDynamic}")
' 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 End If
_logger.Debug($"RenderMultiLineText: Rendering part: [{part}] at Y position: {context.YDynamic}") _logger.Debug($"RenderMultiLineText: Rendering part: [{part}] at Y position: {context.YDynamic}")
context.PDF.DrawText(fontResName, xPos, context.YDynamic, part) context.PDF.DrawText(fontResName, xPos, context.YDynamic, part)
context.YDynamic += LINE_HEIGHT context.YDynamic += LINE_HEIGHT
context.YPlus += LINE_HEIGHT context.YPlus += LINE_HEIGHT
isFirstPart = False
Next Next
Next Next
End Sub End Sub
@@ -821,9 +1017,15 @@ Public Class XRechnungViewDocument
Public Property YPlus As Integer Public Property YPlus As Integer
Public Property CreateTextBox As Boolean Public Property CreateTextBox As Boolean
Public Property CreatedString As String Public Property CreatedString As String
Public Property TaxPosText 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 IsFirstTaxPosDisplay As Boolean
Public Property CurrentPositionStartY As Integer 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) Public Sub New(pdf As GdPicturePDF, createdString As String)
Me.PDF = pdf Me.PDF = pdf
@@ -837,8 +1039,12 @@ Public Class XRechnungViewDocument
YPlus = 0 YPlus = 0
CreateTextBox = False CreateTextBox = False
TaxPosText = "" TaxPosText = ""
TaxPosRate = ""
TaxPosRateCaption = ""
TaxPosAmount = ""
IsFirstTaxPosDisplay = False IsFirstTaxPosDisplay = False
CurrentPositionStartY = 0 CurrentPositionStartY = 0
CurrentPositionPage = 1
End Sub End Sub
End Class End Class

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,10 @@ Namespace Mail
Public Function Connect(pServer As String, pPort As Integer, pUser As String, pPassword As String, pAuthType As String, pOptions As MailSession.MailSessionOptions) As MailSession.SessionInfo Public Function Connect(pServer As String, pPort As Integer, pUser As String, pPassword As String, pAuthType As String, pOptions As MailSession.MailSessionOptions) As MailSession.SessionInfo
Return MailSession.ConnectToServerWithBasicAuth(pServer, pPort, pUser, pPassword, pAuthType, pOptions) Return MailSession.ConnectToServerWithBasicAuth(pServer, pPort, pUser, pPassword, pAuthType, pOptions)
End Function End Function
Public Function ConnectToO365(pUser As String, pClientId As String, pTenantId As String, pClientSecret As String) As MailSession.SessionInfo
Dim oOptions = New MailSession.MailSessionOptions With {.EnableTls1_2 = True}
Return MailSession.ConnectToServerWithO365OAuth(pUser, pClientId, pTenantId, pClientSecret, oOptions)
End Function
Public Function Disconnect() As Boolean Public Function Disconnect() As Boolean
Return MailSession.DisconnectFromServer() Return MailSession.DisconnectFromServer()
End Function End Function

View File

@@ -1,5 +1,4 @@
Imports System.IdentityModel.Tokens Imports System.Net.Security
Imports System.Net.Security
Imports DigitalData.Modules.Base Imports DigitalData.Modules.Base
Imports DigitalData.Modules.Logging Imports DigitalData.Modules.Logging
Imports Limilabs.Client Imports Limilabs.Client
@@ -91,11 +90,11 @@ Namespace Mail
Public Function ConnectToServerWithO365OAuth(pUser As String, pClientId As String, pTenantId As String, pClientSecret As String, pOptions As MailSessionOptions) As SessionInfo Public Function ConnectToServerWithO365OAuth(pUser As String, pClientId As String, pTenantId As String, pClientSecret As String, pOptions As MailSessionOptions) As SessionInfo
' Choose server/port based on the client type ' Choose server/port based on the client type
Dim server As String = If(TypeOf Client Is Smtp, "smtp.office365.com", OAuth2.O365_SERVER) Dim oServer As String = If(TypeOf Client Is Smtp, "smtp-mail.outlook.com", OAuth2.O365_SERVER_IMAP)
Dim port As Integer = If(TypeOf Client Is Imap, 993, If(TypeOf Client Is Smtp, 587, 993)) Dim port As Integer = If(TypeOf Client Is Imap, 993, If(TypeOf Client Is Smtp, 587, 993))
Dim oSession = New SessionInfo With { Dim oSession = New SessionInfo With {
.Server = server, .Server = oServer,
.Port = port, .Port = port,
.ClientId = pClientId, .ClientId = pClientId,
.ClientSecret = pClientSecret, .ClientSecret = pClientSecret,

View File

@@ -10,7 +10,8 @@ Namespace Mail
Private ReadOnly _clientId As String Private ReadOnly _clientId As String
Private ReadOnly _clientSecret As String Private ReadOnly _clientSecret As String
Public Const O365_SERVER As String = "outlook.office365.com" Public Const O365_SERVER_IMAP As String = "outlook.office365.com"
Public Const O365_SERVER_SMTP As String = "smtp-mail.outlook.com"
Public Const O365_SCOPE As String = "https://outlook.office365.com/.default" Public Const O365_SCOPE As String = "https://outlook.office365.com/.default"
Public Const O365_AUTHORITY_PREFIX As String = "https://login.microsoftonline.com/" Public Const O365_AUTHORITY_PREFIX As String = "https://login.microsoftonline.com/"

View File

@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' übernehmen, indem Sie "*" eingeben: ' übernehmen, indem Sie "*" eingeben:
' <Assembly: AssemblyVersion("1.0.*")> ' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.1.0.0")> <Assembly: AssemblyVersion("2.2.0.0")>
<Assembly: AssemblyFileVersion("2.1.0.0")> <Assembly: AssemblyFileVersion("2.2.0.0")>