Refactor stored procedure and update project structure
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:
2026-07-06 00:01:35 +02:00
parent daac828c19
commit 4abf81cd7d
27 changed files with 1544 additions and 94 deletions
+196 -50
View File
@@ -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.