Refactor stored procedure and update project structure
Playwright Tests / test (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
- Modified the stored procedure `fds__admin_getReportCatalog.sql` to use the correct schema for `all_objects`. - Added new folders and projects for `eRechnungLib` in the solution file `Fuchs_Intranet.slnx`, including validation and test projects. - Updated submodule reference for `OCORE`. - Added new submodule `eRechnungLib` with initial commit.
This commit is contained in:
+196
-50
@@ -22,8 +22,23 @@ public static class FuchsPdf
|
||||
public const string ProjectAbbreviation = "fuchs";
|
||||
|
||||
// ── Spire license ─────────────────────────────────────────────────────────
|
||||
public static void SetLicense() =>
|
||||
Spire.License.LicenseProvider.SetLicenseKey(
|
||||
/// <summary>
|
||||
/// Applies the Spire.PDF license. The key is supplied by the caller from the
|
||||
/// <c>SpirePdf-License</c> managed secret (config key <c>SpirePdf_License</c>);
|
||||
/// when no key is provided the embedded fallback key is used so PDF rendering
|
||||
/// still works in local/dev setups without Key Vault access.
|
||||
/// </summary>
|
||||
public static void SetLicense(string? licenseKey = null) =>
|
||||
Spire.License.LicenseProvider.SetLicenseKey(ResolveLicenseKey(licenseKey));
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the effective Spire license key: the managed-secret value when present,
|
||||
/// otherwise the embedded fallback. Pure/side-effect-free for testability.
|
||||
/// </summary>
|
||||
internal static string ResolveLicenseKey(string? licenseKey) =>
|
||||
string.IsNullOrWhiteSpace(licenseKey) ? EmbeddedLicenseKey : licenseKey;
|
||||
|
||||
private const string EmbeddedLicenseKey =
|
||||
"I+ztXu/77JVCXwEAwVQwRISgL4qlo1lOxO6csGdd02iJsOnMzEkqjhRx6oJ5rw5fgaF5wUf83LWMWwLE8PNc" +
|
||||
"/ZGUZIa8mTx9ovjM9fK2+xLk/VC3s555Qhd5+PLfgxIEsp4r6lw03P7YPvD6pvM745VQg0dd8thRoznmkWrkUf" +
|
||||
"/2/MiUZyUyVrH+qyEZgkniqpuDdqoaUNx1RfsK6TyiKKB7nsiqDy9xrduuYCMgOg1wii3aU+anA/pHUYh/jMO0" +
|
||||
@@ -41,7 +56,7 @@ public static class FuchsPdf
|
||||
"uUg5LlJmPPXkTKHQJ/CM6EQkqIS4Foz7pBaaYRBgEz/zDujxbYUGN6LaJiANung4Zyl6k5arhHdCalRDe29avN1o" +
|
||||
"vxe/5tUHQQDxq+yQ1cNChPJTFHR1bKKu0T7SW7p19qH5850rXcjtzK4+6zGYXq8HItH6UNiev27o9VUoKTv+XZiD" +
|
||||
"27YE33vdwQHh5Kdc8CMMo+uaTI11uLBirUH63Na2oBkCGJjJzQk8Gc5NQs7+2DptJ/rNlOhwb/czZLB6OjH+vNCy" +
|
||||
"HZBCGPd17rIW16JQzgWv+OBI9DbD7pXYzDyF++IrBiRKBPNKCTwg3trm89J4zWeGW80bFtD0QnIcArA==");
|
||||
"HZBCGPd17rIW16JQzgWv+OBI9DbD7pXYzDyF++IrBiRKBPNKCTwg3trm89J4zWeGW80bFtD0QnIcArA==";
|
||||
|
||||
// ── Colors ────────────────────────────────────────────────────────────────
|
||||
private static readonly Color FuchsGray = Color.FromRgb(128, 128, 128);
|
||||
@@ -193,21 +208,36 @@ public static class FuchsPdf
|
||||
DefineStyles_Letter(doc, Array.Empty<Style>(), tgtFont);
|
||||
|
||||
var section = doc.AddSection();
|
||||
section.PageSetup.TopMargin = cm(1.8);
|
||||
section.PageSetup.BottomMargin = cm(1.8);
|
||||
section.PageSetup.PageHeight = doc.DefaultPageSetup.PageHeight;
|
||||
section.PageSetup.PageWidth = doc.DefaultPageSetup.PageWidth;
|
||||
|
||||
// Bottom margin reserves room for the four footer blocks + page-number row,
|
||||
// exactly as the legacy layout computed it (22.5mm footer block + 2× the ISO
|
||||
// page-number margin + one page-number row). A too-small bottom margin was one
|
||||
// cause of body text overrunning the footer.
|
||||
Unit isoPageNumMargin = mm(10);
|
||||
Unit pageNumRowHeight = mm(doc.Styles["PageNumStyle"]!.Font.Size.Millimeter);
|
||||
section.PageSetup.TopMargin = cm(2.0);
|
||||
section.PageSetup.BottomMargin = new Unit(
|
||||
22.5 + 2 * isoPageNumMargin.Millimeter + pageNumRowHeight.Millimeter, UnitType.Millimeter);
|
||||
section.PageSetup.LeftMargin = cm(2.5);
|
||||
section.PageSetup.RightMargin = cm(2.0);
|
||||
section.PageSetup.DifferentFirstPageHeaderFooter = true;
|
||||
section.PageSetup.HeaderDistance = cm(0);
|
||||
|
||||
string dataBase = Path.Combine(AppContext.BaseDirectory, "Data");
|
||||
|
||||
// ── Header logos ──────────────────────────────────────────────────────
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image1.png"),
|
||||
width: mm(155.5), top: cm(0.79), left: cm(2.0));
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image2.png"),
|
||||
width: mm(34.5), top: cm(0.59), left: cm(15.27));
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image3.png"),
|
||||
width: mm(25.4), top: cm(6.21), left: cm(17.51));
|
||||
// ── Header logos (top-right corner of the letterhead) ─────────────────
|
||||
// NOTE: the shipped assets are image1-3.jpeg + image4.png. Referencing the
|
||||
// wrong extension made AddHeaderImage silently skip them (File.Exists == false),
|
||||
// which is why the letterhead logos vanished. Sizes/positions match the legacy
|
||||
// CreatePage_letter.
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image1.jpeg"),
|
||||
width: mm(39.3), top: cm(1.73), left: cm(16.07));
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image2.jpeg"),
|
||||
width: mm(25.4), top: cm(4.89), left: cm(17.5));
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image3.jpeg"),
|
||||
width: mm(26.0), top: cm(6.21), left: cm(17.51));
|
||||
AddHeaderImage(section.Headers.FirstPage, Path.Combine(dataBase, "image4.png"),
|
||||
width: mm(25.4), top: cm(7.79), left: cm(17.5));
|
||||
|
||||
@@ -243,12 +273,16 @@ public static class FuchsPdf
|
||||
{
|
||||
var tf = section.Headers.FirstPage.AddTextFrame();
|
||||
tf.RelativeVertical = RelativeVertical.Page;
|
||||
tf.RelativeHorizontal = RelativeHorizontal.Page;
|
||||
tf.Top = cm(4.65); tf.Left = cm(2.0);
|
||||
tf.Width = cm(8.5); tf.Height = cm(0.6);
|
||||
tf.RelativeHorizontal = RelativeHorizontal.Margin;
|
||||
tf.Left = ShapePosition.Left;
|
||||
tf.Top = cm(5.3);
|
||||
tf.Width = cm(14); tf.Height = mm(12.5);
|
||||
var p = tf.AddParagraph();
|
||||
p.Style = "AddressBoxSender";
|
||||
p.AddText($"{tb.SenderLine1} \u25cf {tb.SenderLine2}");
|
||||
if (!string.IsNullOrEmpty(tb.SenderLine1))
|
||||
p.AddFormattedText(tb.SenderLine1, TextFormat.Bold);
|
||||
p.AddText(" " + tb.SenderLine2);
|
||||
tf.WrapFormat.Style = WrapStyle.Through;
|
||||
}
|
||||
|
||||
// ── Recipient address box ─────────────────────────────────────────────
|
||||
@@ -258,6 +292,7 @@ public static class FuchsPdf
|
||||
tf.RelativeHorizontal = RelativeHorizontal.Page;
|
||||
tf.Top = cm(5.6); tf.Left = cm(2.0);
|
||||
tf.Width = cm(9); tf.Height = cm(4);
|
||||
tf.MarginLeft = cm(0.5); tf.MarginRight = cm(0.5); tf.MarginBottom = cm(0.5);
|
||||
tf.MarginTop = cm(0.2);
|
||||
if (tb.Address.Length > 0)
|
||||
{
|
||||
@@ -267,26 +302,28 @@ public static class FuchsPdf
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin info block (right side) ─────────────────────────────────────
|
||||
// ── Admin info block (right column, label over value) ─────────────────
|
||||
{
|
||||
var tf = section.Headers.FirstPage.AddTextFrame();
|
||||
tf.RelativeVertical = RelativeVertical.Page;
|
||||
tf.RelativeHorizontal = RelativeHorizontal.Page;
|
||||
tf.Top = cm(5.6); tf.Left = cm(13.0);
|
||||
tf.Width = cm(5.5); tf.Height = cm(5.5);
|
||||
void Row(string label, string value)
|
||||
tf.Top = mm(52.5); tf.Left = cm(12.87);
|
||||
tf.Width = cm(5); tf.Height = cm(12);
|
||||
bool first = true;
|
||||
void Block(string label, string value)
|
||||
{
|
||||
var p = tf.AddParagraph(); p.Style = "AdminInfo";
|
||||
p.AddFormattedText(label + ": ", TextFormat.Bold);
|
||||
var p = tf.AddParagraph(); p.Style = "AdminBlock";
|
||||
if (first) { p.Format.SpaceBefore = 0; first = false; }
|
||||
p.AddFormattedText(label, "AdminBlockHead");
|
||||
p.AddLineBreak();
|
||||
p.AddText(value);
|
||||
}
|
||||
Row(tb.AdminDatumLabel, tb.AdminDatum);
|
||||
if (!string.IsNullOrEmpty(tb.AdminRef))
|
||||
Row("Nummer", tb.AdminRef);
|
||||
if (!string.IsNullOrEmpty(tb.ProvisionPeriod))
|
||||
Row(tb.AdminProvLabel, tb.ProvisionPeriod);
|
||||
Row("Sachbearbeiter", tb.AdminUser);
|
||||
Row("E-Mail", tb.AdminUserEmail);
|
||||
Block("Bearbeiter", (tb.AdminUser ?? "").ne(" "));
|
||||
Block("Email", (tb.AdminUserEmail ?? "").ne("-"));
|
||||
Block(tb.AdminDatumLabel, (tb.AdminDatum ?? "").ne("-"));
|
||||
Block(tb.AdminProvLabel, (tb.ProvisionPeriod ?? "").ne("-"));
|
||||
Block("Nummer", (tb.AdminRef ?? "").ne("-"));
|
||||
tf.WrapFormat.Style = WrapStyle.Through;
|
||||
}
|
||||
|
||||
// ── Ort und Zeit ──────────────────────────────────────────────────────
|
||||
@@ -304,10 +341,25 @@ public static class FuchsPdf
|
||||
p.Format.Alignment = ParagraphAlignment.Right;
|
||||
}
|
||||
|
||||
// ── Footer (all pages) ────────────────────────────────────────────────
|
||||
AddLetterFooter(section.Footers.Primary, tb, tgtFont);
|
||||
AddLetterFooter(section.Footers.FirstPage, tb, tgtFont);
|
||||
AddLetterFooter(section.Footers.EvenPage, tb, tgtFont);
|
||||
// ── Footer blocks + page numbers (first page + all following pages) ───
|
||||
AddLetterFooterBlocks(section, section.Footers.Primary, tb);
|
||||
AddLetterFooterBlocks(section, section.Footers.FirstPage, tb);
|
||||
AddPageNumber(section, section.Footers.Primary, isoPageNumMargin, pageNumRowHeight);
|
||||
AddPageNumber(section, section.Footers.FirstPage, isoPageNumMargin, pageNumRowHeight);
|
||||
|
||||
// MigraDoc drops SpaceBefore on the very first body paragraph of a page. The letter
|
||||
// body must start ~10 cm down (below the floating address window / admin block), which
|
||||
// the invoice title / subject achieve via a large SpaceBefore — but only if they are
|
||||
// not the first paragraph. This tiny empty anchor absorbs the first-paragraph
|
||||
// suppression so the following content's SpaceBefore (the letterhead offset) is honored.
|
||||
{
|
||||
var anchor = section.AddParagraph();
|
||||
anchor.Format.Font.Size = 1;
|
||||
anchor.Format.SpaceBefore = 0;
|
||||
anchor.Format.SpaceAfter = 0;
|
||||
anchor.Format.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
anchor.Format.LineSpacing = pt(1);
|
||||
}
|
||||
|
||||
// ── Subject + body ────────────────────────────────────────────────────
|
||||
if (!string.IsNullOrEmpty(tb.Subject))
|
||||
@@ -339,8 +391,10 @@ public static class FuchsPdf
|
||||
normal.Font.Name = tgtFont;
|
||||
normal.Font.Size = 11;
|
||||
|
||||
AddStyle(doc, "PageNumStyle", "Normal", s => s.Font.Size = 9);
|
||||
AddStyle(doc, "PageNumStyle", "Normal", s => s.Font.Size = 10);
|
||||
AddStyle(doc, "BodyText", "Normal", s => { s.Font.Size = 11; s.ParagraphFormat.LineSpacing = 1.15; s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Multiple; });
|
||||
// Base style for MigraDoc tables (invoice/reminder item grids reference "Table").
|
||||
AddStyle(doc, "Table", "Normal", s => { s.Font.Name = tgtFont; s.Font.Size = 11; s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Single; });
|
||||
AddStyle(doc, "AddressBox", "Normal", s => { s.Font.Size = 10; s.Font.Name = tgtFont; });
|
||||
AddStyle(doc, "AddressBoxSender", "Normal", s => { s.Font.Size = 7; s.Font.Name = tgtFont; s.Font.Color = FuchsGray; });
|
||||
AddStyle(doc, "AdminInfo", "Normal", s => s.Font.Size = 9);
|
||||
@@ -367,6 +421,51 @@ public static class FuchsPdf
|
||||
{
|
||||
if (!SystemFontExists(tgtFont)) tgtFont = "Arial";
|
||||
|
||||
var normal = doc.Styles["Normal"]!;
|
||||
normal.Font.Name = tgtFont;
|
||||
normal.Font.Color = Colors.Black;
|
||||
|
||||
AddStyle(doc, "BodyText", "Normal", s => { s.Font.Name = tgtFont; s.Font.Size = baseSize; });
|
||||
|
||||
// ── Letterhead frame styles (ported 1:1 from legacy fuchs_fds_pdf.vb) ──
|
||||
AddStyle(doc, "AdminBlockHead", "Normal", s =>
|
||||
s.Font = new Font(tgtFont, 9) { Color = FuchsGray, Bold = true });
|
||||
AddStyle(doc, "AdminBlock", "Normal", s =>
|
||||
{
|
||||
s.Font = new Font(tgtFont, 9) { Color = Colors.Black, Bold = false };
|
||||
s.ParagraphFormat.SpaceBefore = cm(0.25);
|
||||
s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
s.ParagraphFormat.LineSpacing = pt(9 * 1.2);
|
||||
});
|
||||
AddStyle(doc, "FooterBlock", "Normal", s =>
|
||||
{
|
||||
s.Font = new Font(tgtFont, 7) { Color = FuchsBlau };
|
||||
s.ParagraphFormat.Alignment = ParagraphAlignment.Left;
|
||||
s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
s.ParagraphFormat.LineSpacing = pt(7.5);
|
||||
});
|
||||
AddStyle(doc, "AddressBoxSender", "Normal", s =>
|
||||
{
|
||||
s.Font = new Font(tgtFont, 7.5) { Color = FuchsBlau };
|
||||
s.ParagraphFormat.Alignment = ParagraphAlignment.Left;
|
||||
s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
s.ParagraphFormat.LineSpacing = pt(7.5);
|
||||
});
|
||||
AddStyle(doc, "AddressBox", "Normal", s =>
|
||||
{
|
||||
s.Font = new Font(tgtFont, 10) { Color = Colors.Black };
|
||||
s.ParagraphFormat.Alignment = ParagraphAlignment.Left;
|
||||
s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
s.ParagraphFormat.LineSpacing = pt(12);
|
||||
});
|
||||
AddStyle(doc, "PageNumStyle", "Normal", s =>
|
||||
{
|
||||
s.Font = new Font(tgtFont, 10) { Color = Colors.Black };
|
||||
s.ParagraphFormat.Alignment = ParagraphAlignment.Right;
|
||||
s.ParagraphFormat.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
s.ParagraphFormat.LineSpacing = pt(10);
|
||||
});
|
||||
|
||||
AddStyle(doc, "SubjectBig", "Normal", s => { s.Font.Name = tgtFont; s.Font.Size = 13; s.Font.Bold = true; });
|
||||
AddStyle(doc, "HorizontalRule", "Normal", s =>
|
||||
{
|
||||
@@ -618,9 +717,20 @@ public static class FuchsPdf
|
||||
// ── ApplyReminder ─────────────────────────────────────────────────────────
|
||||
public static void ApplyReminder(Document doc, FdsTextBlocks tb, FdsReminderData rem, bool draft = false)
|
||||
{
|
||||
Apply_Invoice_Styles(doc);
|
||||
var sec = doc.Sections.Cast<Section>().First();
|
||||
string rtype = rem.ReminderType;
|
||||
|
||||
// Title — carries the letterhead top offset so the body starts below the address
|
||||
// window / admin block (see the anchor note in CreatePage_Letter).
|
||||
{
|
||||
var p = sec.AddParagraph();
|
||||
p.Style = "SubjectBig";
|
||||
p.Format.SpaceBefore = cm(8.65);
|
||||
p.Format.SpaceAfter = cm(0.5);
|
||||
p.AddText(rem.ReminderTitle.ne("Zahlungserinnerung"));
|
||||
}
|
||||
|
||||
// Opening text
|
||||
if (tb.ReminderTexts_before.TryGetValue(rtype, out var intro))
|
||||
{
|
||||
@@ -706,9 +816,24 @@ public static class FuchsPdf
|
||||
|
||||
// ── PDF rendering helpers ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Ensures PdfSharp's global font resolver is the OCORE one before any MigraDoc rendering.
|
||||
/// PdfSharp 6+ no longer resolves system fonts (e.g. "Courier New") on its own — without this,
|
||||
/// PdfDocumentRenderer.RenderDocument() throws "cannot be resolved for predefined error font".
|
||||
/// </summary>
|
||||
private static void EnsureFontResolver()
|
||||
{
|
||||
if (PdfSharp.Fonts.GlobalFontSettings.FontResolver == null ||
|
||||
PdfSharp.Fonts.GlobalFontSettings.FontResolver.GetType() != typeof(OCORE_web_pdf.pdf.OCOREFontResolver))
|
||||
{
|
||||
PdfSharp.Fonts.GlobalFontSettings.FontResolver = new OCORE_web_pdf.pdf.OCOREFontResolver();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Renders a MigraDoc Document to a PDF/A byte array.</summary>
|
||||
public static byte[] DocToPdfBytes(Document doc)
|
||||
{
|
||||
EnsureFontResolver();
|
||||
var renderer = new PdfDocumentRenderer() { Document = doc };
|
||||
renderer.RenderDocument();
|
||||
using var ms = new MemoryStream();
|
||||
@@ -771,30 +896,51 @@ public static class FuchsPdf
|
||||
img.WrapFormat.Style = WrapStyle.Through;
|
||||
}
|
||||
|
||||
private static void AddLetterFooter(HeaderFooter footer, FdsTextBlocks tb, string font)
|
||||
/// <summary>
|
||||
/// Draws the four bottom-of-page footer text blocks (company / liability / contact / bank)
|
||||
/// as absolutely positioned text frames, matching the legacy fuchs_fds_pdf.vb layout.
|
||||
/// </summary>
|
||||
private static void AddLetterFooterBlocks(Section section, HeaderFooter footer, FdsTextBlocks tb)
|
||||
{
|
||||
// Horizontal rule
|
||||
var rule = footer.AddParagraph(); rule.Style = "FooterText";
|
||||
rule.Format.Borders.Top.Width = 0.5;
|
||||
rule.Format.Borders.Top.Color = FuchsGray;
|
||||
rule.Format.SpaceBefore = 4;
|
||||
|
||||
// Four-column footer table
|
||||
var tbl = footer.AddTable();
|
||||
tbl.Format.Font.Name = font; tbl.Format.Font.Size = 8;
|
||||
tbl.Format.Font.Color = FuchsGray;
|
||||
tbl.Borders.Visible = false;
|
||||
double[] widths = { 4.2, 4.8, 4.2, 4.8 };
|
||||
foreach (double w in widths) tbl.AddColumn(cm(w));
|
||||
var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto;
|
||||
string[][] blocks = { tb.FooterBlock1, tb.FooterBlock2, tb.FooterBlock3, tb.FooterBlock4 };
|
||||
double topMm = section.PageSetup.PageHeight.Millimeter - 25;
|
||||
for (int col = 0; col < 4; col++)
|
||||
{
|
||||
var p = row.Cells[col].AddParagraph();
|
||||
var tf = footer.AddTextFrame();
|
||||
tf.RelativeHorizontal = RelativeHorizontal.Page;
|
||||
tf.RelativeVertical = RelativeVertical.Page;
|
||||
tf.Left = cm(2.5 + col * 4.25);
|
||||
tf.Top = mm(topMm);
|
||||
tf.Width = cm(4.5);
|
||||
tf.Height = cm(2);
|
||||
tf.MarginTop = 0; tf.MarginBottom = 0; tf.MarginLeft = 0; tf.MarginRight = 0;
|
||||
var p = tf.AddParagraph();
|
||||
p.Style = "FooterBlock";
|
||||
foreach (string line in blocks[col]) { p.AddText(line); p.AddLineBreak(); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds the right-aligned "Seite X von Y" page-number frame below the bottom margin.</summary>
|
||||
private static void AddPageNumber(Section section, HeaderFooter footer, Unit isoMargin, Unit rowHeight)
|
||||
{
|
||||
var tf = footer.AddTextFrame();
|
||||
tf.RelativeHorizontal = RelativeHorizontal.Margin;
|
||||
tf.RelativeVertical = RelativeVertical.Page;
|
||||
tf.Left = ShapePosition.Right;
|
||||
tf.Top = mm(section.PageSetup.PageHeight.Millimeter
|
||||
- section.PageSetup.BottomMargin.Millimeter + isoMargin.Millimeter);
|
||||
tf.Width = cm(4.5);
|
||||
tf.Height = rowHeight;
|
||||
tf.MarginTop = 0; tf.MarginBottom = 0; tf.MarginLeft = 0; tf.MarginRight = 0;
|
||||
var p = tf.AddParagraph();
|
||||
p.Style = "PageNumStyle";
|
||||
p.Format.Alignment = ParagraphAlignment.Right;
|
||||
p.AddText("Seite ");
|
||||
p.AddPageField();
|
||||
p.AddText(" von ");
|
||||
p.AddNumPagesField();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the SEPA "GiroCode" payment QR (EPC069-12) into a two-column box,
|
||||
/// matching the legacy fuchs_fds_pdf.vb invoice/reminder layout. No-op on failure.
|
||||
|
||||
Reference in New Issue
Block a user