added improvements on set proces and minor other improvements
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static OCORE.OCORE_dictionaries;
|
||||
@@ -171,7 +171,11 @@ public class FdsInvoiceData
|
||||
var tokens = new List<string>();
|
||||
if (Admin?.no("p13b", false) is true) tokens.Add("§13b");
|
||||
string setmode = (Admin?.nz("setmode") ?? "").Trim().ToLowerInvariant();
|
||||
if (setmode is "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
|
||||
// Persist any explicitly-chosen mode, including the default "setprice" — otherwise an
|
||||
// explicit user choice of the default is indistinguishable from an invoice that was never
|
||||
// switched to set-pricing at all (needed so the editor can hide the "Set-Preisanzeige"
|
||||
// menu entry once a mode has been chosen; see InvoiceDraftEditService.BuildInvoiceOptions).
|
||||
if (setmode is "setprice" or "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
|
||||
return string.Join(",", tokens);
|
||||
}
|
||||
|
||||
|
||||
+247
-169
@@ -112,21 +112,40 @@ public static class FuchsPdf
|
||||
"Kreissparkasse D\u00fcsseldorf", "IBAN: DE52 3015 0200 0002 0914 78", "BIC: WELADED1KSD",
|
||||
"Stadtsparkasse D\u00fcsseldorf", "IBAN: DE76 3005 0110 0045 0148 00", "BIC: DUSSDEDDXXX"
|
||||
};
|
||||
// Reminder greeting/intro, keyed by the actual reminder type (f = Zahlungserinnerung,
|
||||
// m = Mahnung, l = letzte außergerichtliche Mahnung) — the single source of truth for
|
||||
// both the PDF (ApplyReminder) and the online editor (via BuildReminderNotes).
|
||||
public Dictionary<string, string[]> ReminderTexts_before { get; } = new()
|
||||
{
|
||||
["f"] = new[] {
|
||||
"Sehr geehrte Damen und Herren,",
|
||||
"wir erlauben uns, Sie auf die nachfolgende offene Rechnung aufmerksam zu machen."
|
||||
"ein Mahnschreiben sollte kurz, freundlich und erfolgreich sein. Kurz ist es, freundlich sowieso; ob es auch erfolgreich ist, h\u00e4ngt von Ihnen ab."
|
||||
},
|
||||
["s"] = new[] {
|
||||
["m"] = new[] {
|
||||
"Sehr geehrte Damen und Herren,",
|
||||
"trotz unserer Ersten Mahnung haben wir bis heute noch keinen Zahlungseingang feststellen k\u00f6nnen. " +
|
||||
"Wir bitten Sie, den offenen Betrag inkl. der Mahnzinsen und Mahnkosten umgehend auf unser Konto zu \u00fcberweisen."
|
||||
"nun m\u00fcssen wir Sie noch einmal anschreiben.",
|
||||
"Wahrscheinlich haben Sie triftige Gr\u00fcnde daf\u00fcr, warum Sie die Zahlung unserer Forderung nicht vornehmen und auch nicht auf unsere Mahnung reagieren. Sollten wir dar\u00fcber nicht einmal sprechen?",
|
||||
"Bitte nehmen Sie umgehend in dieser Sache mit uns Kontakt auf."
|
||||
},
|
||||
["l"] = Array.Empty<string>()
|
||||
["l"] = new[] {
|
||||
"Sehr geehrte Damen und Herren,",
|
||||
"Eine DRITTE MAHNUNG zu erhalten bereitet Ihnen bestimmt ebenso wenig Freude wie uns, sie zu verschicken. Leider haben wir auf unsere zweite Mahnung noch keine Antwort von Ihnen erhalten.",
|
||||
"Wir bitten Sie, den offenen Betrag innerhalb der n\u00e4chsten 7 Werktage nach Erhalt dieses Schreibens zu begleichen. Nach Ablauf dieser Frist erfolgt keine weitere Mahnung mehr.",
|
||||
"Sollte die Forderung bis dahin nicht beglichen sein, er\u00f6ffnen wir das gerichtliche Mahnverfahren. Sollten Sie die Rechnung inzwischen beglichen haben, so betrachten Sie bitte dieses Schreiben als gegenstandslos."
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>Extra closing sentence appended for reminder type <c>m</c> (Mahnung) — <c>f</c>'s closing is generated
|
||||
/// dynamically (amount/IBAN + "gegenstandslos"); <c>l</c> gets no extra closing note.</summary>
|
||||
public Dictionary<string, string> ReminderClosingExtra { get; } = new()
|
||||
{
|
||||
["m"] = "Um Ihnen zus\u00e4tzliche Kosten f\u00fcr weitere Mahnungen zu ersparen, bitten wir Sie nunmehr um die \u00dcberweisung des noch zu zahlenden Gesamtbetrages inklusive der ggf. bereits f\u00e4lligen Mahnzinsen und Mahngeb\u00fchren innerhalb von einer Woche."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>One notice/explanatory paragraph, as rendered both in the PDF and the online editor.</summary>
|
||||
public sealed record PdfNote(string Text, string Style = "InvoiceNotes");
|
||||
|
||||
// ── Currency / parse helpers ───────────────────────────────────────────────
|
||||
public static string Currency(object? input, string returnobject = "?")
|
||||
{
|
||||
@@ -529,7 +548,7 @@ public static class FuchsPdf
|
||||
ParagraphFormat ClonePf()
|
||||
{
|
||||
var pf = doc.Styles["Normal"]!.ParagraphFormat.Clone();
|
||||
pf.Borders.Distance = 3;
|
||||
pf.Borders.Distance = 0;
|
||||
pf.Alignment = ParagraphAlignment.Left;
|
||||
return pf;
|
||||
}
|
||||
@@ -557,145 +576,21 @@ public static class FuchsPdf
|
||||
AddStyle(doc, "InvoiceNotes_ucb", "InvoiceNotes", s => s.Font = new Font(tgtFont, 9) { Bold = true });
|
||||
}
|
||||
|
||||
// ── ApplyInvoice ──────────────────────────────────────────────────────────
|
||||
public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false)
|
||||
/// <summary>
|
||||
/// Builds the ordered list of standard invoice notice paragraphs (ported from legacy
|
||||
/// fuchs_fds_pdf.vb) — pure data, no MigraDoc dependency, so it can be reused by both
|
||||
/// <see cref="ApplyInvoice"/> and the online editor's draft view-state (ADR: notice texts
|
||||
/// are backend-authoritative — the editor renders exactly what the PDF will show).
|
||||
/// </summary>
|
||||
public static List<PdfNote> BuildInvoiceNotes(FdsTextBlocks tb, FdsInvoiceData inv, bool p13b)
|
||||
{
|
||||
string invoiceType = inv.InvoiceType;
|
||||
bool p13b = inv.InvoiceRegistration?.getString("InvoiceOptions")
|
||||
.Split(',').Contains("§13b") ?? false;
|
||||
Apply_Invoice_Styles(doc);
|
||||
|
||||
var sec = doc.Sections.Cast<Section>().First();
|
||||
sec.PageSetup.RightMargin = cm(1.5);
|
||||
|
||||
// Title
|
||||
{
|
||||
var p = sec.AddParagraph();
|
||||
p.Style = "SubjectBig";
|
||||
p.Format.SpaceBefore = cm(8.65);
|
||||
p.AddText($"{inv.InvoiceTitle} Nr. {inv.InvoiceId}");
|
||||
p.Format.SpaceAfter = cm(0.5);
|
||||
}
|
||||
|
||||
// Provision location
|
||||
if (inv.ProvisionLocation.Length > 0)
|
||||
{
|
||||
var p = sec.AddParagraph();
|
||||
p.Style = "AddressBox";
|
||||
p.Format.Font.Size = 10;
|
||||
p.Format.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
p.Format.LineSpacing = 12;
|
||||
p.AddFormattedText(tb.ProvisionLoc_Label, TextFormat.Bold);
|
||||
p.AddLineBreak();
|
||||
foreach (string t in inv.ProvisionLocation) { p.AddText(t); p.AddLineBreak(); }
|
||||
}
|
||||
sec.AddParagraph().Format.SpaceBefore = cm(0.7);
|
||||
|
||||
// Invoice items table
|
||||
var tbl = sec.AddTable();
|
||||
tbl.Style = "Table";
|
||||
tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1);
|
||||
tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25;
|
||||
tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5;
|
||||
tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6);
|
||||
tbl.Rows.HeightRule = RowHeightRule.AtLeast;
|
||||
|
||||
double pageW = sec.PageSetup.PageWidth.Centimeter
|
||||
- sec.PageSetup.LeftMargin.Centimeter
|
||||
- sec.PageSetup.RightMargin.Centimeter;
|
||||
double[] colWidths = { pageW * 0.06, pageW * 0.42, pageW * 0.12, pageW * 0.18, pageW * 0.22 };
|
||||
foreach (double w in colWidths) tbl.AddColumn(cm(w));
|
||||
|
||||
// Header row
|
||||
string[] headers = { "Pos.", "Bezeichnung", "Menge", "Einzelpreis", "Gesamtpreis" };
|
||||
var hRow = tbl.AddRow();
|
||||
hRow.Shading = GrayShading();
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
var p = hRow.Cells[i].AddParagraph(); p.Style = "TblCell_Head";
|
||||
p.AddFormattedText(headers[i], TextFormat.Bold);
|
||||
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
|
||||
}
|
||||
|
||||
// Data rows — grouped by service-request block (see FdsInvoiceData.InvoiceBlocks).
|
||||
// Per the product decision the PDF must mirror the online editor exactly: each section
|
||||
// prints its heading, every position shows its own price, and positions are numbered the
|
||||
// same way the editor numbers them (every line except free-text/heading lines, including a
|
||||
// set header). Set-display collapsing is honoured only for the explicit SetOnly mode; every
|
||||
// other mode renders the items flat, so nothing is silently blanked or renumbered.
|
||||
var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions"));
|
||||
int pos = 0;
|
||||
foreach (var block in inv.InvoiceBlocks)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(block.Heading))
|
||||
{
|
||||
var hr = tbl.AddRow();
|
||||
hr.HeightRule = RowHeightRule.Auto;
|
||||
hr.Cells[1].MergeRight = 3; // span Bezeichnung … Gesamtpreis
|
||||
hr.Cells[1].AddParagraph().WithStyle("TblCell_RTitle").AddFormattedText(block.Heading, TextFormat.Bold);
|
||||
}
|
||||
|
||||
var lines = setMode == SetDisplayMode.SetOnly && InvoiceSetPricing.ContainsSets(block.Items)
|
||||
? InvoiceSetPricing.Build(block.Items, SetDisplayMode.SetOnly) // only this mode collapses members
|
||||
: block.Items.Select(MapItemToLine).ToList(); // flat: faithful mirror of the editor
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
bool numbered = line.IsSetHeader || line.ShowPrice; // free-text/heading lines carry no number
|
||||
var row = tbl.AddRow();
|
||||
row.HeightRule = RowHeightRule.Auto;
|
||||
row.Cells[0].AddParagraph(numbered ? (++pos).ToString() : "").Style = "TblCell_Base";
|
||||
if (!string.IsNullOrEmpty(line.Title)) // skip the empty paragraph that added a blank line before free text
|
||||
{
|
||||
var titleCell = row.Cells[1].AddParagraph();
|
||||
titleCell.Style = "TblCell_RTitle";
|
||||
if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold);
|
||||
else titleCell.AddText(line.Title);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"<div>{line.Desc}</div>");
|
||||
row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base";
|
||||
row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base";
|
||||
row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.TotalNet) : "").Style = "TblCell_RSum";
|
||||
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
|
||||
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
|
||||
row.Cells[4].Format.Alignment = ParagraphAlignment.Right;
|
||||
}
|
||||
}
|
||||
|
||||
// Totals
|
||||
void TotalRow(string label, string value, string style = "TblCell_TSum", bool topBorder = false)
|
||||
{
|
||||
var r = tbl.AddRow(); r.Shading = LightShading();
|
||||
if (topBorder) r.Borders.Top.Width = 0.5;
|
||||
var lp = r.Cells[3].AddParagraph(); lp.Style = style; lp.AddText(label);
|
||||
r.Cells[3].Format.Alignment = ParagraphAlignment.Left;
|
||||
var vp = r.Cells[4].AddParagraph(); vp.Style = style; vp.AddText(value);
|
||||
r.Cells[4].Format.Alignment = ParagraphAlignment.Right;
|
||||
}
|
||||
|
||||
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance_net"), out decimal netSum);
|
||||
TotalRow("Nettobetrag:", Currency(netSum), "TblCell_TNet", topBorder: true);
|
||||
|
||||
// VAT rows
|
||||
foreach (var vr in inv.VatRows)
|
||||
{
|
||||
string pct = vr.Key;
|
||||
ParseDec(vr.Value.no("vat_amount", 0), out decimal vatAmt);
|
||||
TotalRow($"MwSt. {pct}%:", Currency(vatAmt), "TblCell_TVat");
|
||||
}
|
||||
|
||||
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal gross);
|
||||
TotalRow("Rechnungsbetrag:", Currency(gross), "TblCell_TSum");
|
||||
|
||||
|
||||
// ── Standard invoice notes (ported from legacy fuchs_fds_pdf.vb) ──────────
|
||||
var reg = inv.InvoiceRegistration;
|
||||
var notes = new List<PdfNote>();
|
||||
void Note(string text, string style = "InvoiceNotes")
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
var np = sec.AddParagraph(); np.Style = style; np.AddText(text);
|
||||
if (!string.IsNullOrEmpty(text)) notes.Add(new PdfNote(text, style));
|
||||
}
|
||||
|
||||
var reg = inv.InvoiceRegistration;
|
||||
ParseDec(reg?.getItem("InvoiceService"), out decimal invService);
|
||||
string serviceGross = Currency(reg?.getItem("InvoiceService"));
|
||||
|
||||
@@ -772,8 +667,179 @@ public static class FuchsPdf
|
||||
"die Umwelt zu schützen. Deshalb konzentrieren wir uns auf die Aufforstung von Flächen.");
|
||||
Note("Wir bedanken uns herzlich für Ihren Auftrag.");
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the reminder intro/closing notice paragraphs (greeting + closing bank/amount and
|
||||
/// "gegenstandslos"/Mahnzinsen sentences), keyed by the actual reminder type. Pure data — no
|
||||
/// MigraDoc dependency — reused by <see cref="ApplyReminder"/> and the online editor's draft
|
||||
/// view-state so the editor shows exactly what the PDF will show.
|
||||
/// </summary>
|
||||
public static (string[] Intro, string[] Closing) BuildReminderNotes(FdsTextBlocks tb, FdsReminderData rem)
|
||||
{
|
||||
string rtype = rem.ReminderType;
|
||||
string[] intro = tb.ReminderTexts_before.TryGetValue(rtype, out var i) ? i : Array.Empty<string>();
|
||||
|
||||
ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal);
|
||||
string terms = TranslatePaymentTerm(rem.ReminderRegistration?.getString("PaymentTerm") ?? "");
|
||||
|
||||
var closing = new List<string>();
|
||||
switch (rtype)
|
||||
{
|
||||
case "f":
|
||||
closing.Add(
|
||||
$"Bitte überweisen Sie den offenen Betrag von {Currency(openTotal)} innerhalb von {terms} auf unser Konto:\n" +
|
||||
"IBAN: DE76\u00a03005\u00a00110\u00a00045\u00a00148\u00a000, BIC DUSSSDEDDXXX (Stadtsparkasse Düsseldorf)");
|
||||
closing.Add("Sollten Sie den Betrag bereits überwiesen haben, so bitten wir Sie, diese Zahlungserinnerung als gegenstandslos zu betrachten.");
|
||||
break;
|
||||
default:
|
||||
if (tb.ReminderClosingExtra.TryGetValue(rtype, out var extra)) closing.Add(extra);
|
||||
break;
|
||||
}
|
||||
return (intro, closing.ToArray());
|
||||
}
|
||||
|
||||
// ── ApplyInvoice ──────────────────────────────────────────────────────────
|
||||
public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false)
|
||||
{
|
||||
string invoiceType = inv.InvoiceType;
|
||||
bool p13b = inv.InvoiceRegistration?.getString("InvoiceOptions")
|
||||
.Split(',').Contains("§13b") ?? false;
|
||||
Apply_Invoice_Styles(doc);
|
||||
|
||||
var sec = doc.Sections.Cast<Section>().First();
|
||||
sec.PageSetup.RightMargin = cm(1.5);
|
||||
|
||||
// Title
|
||||
{
|
||||
var p = sec.AddParagraph();
|
||||
p.Style = "SubjectBig";
|
||||
p.Format.SpaceBefore = cm(8.65);
|
||||
p.AddText($"{inv.InvoiceTitle} Nr. {inv.InvoiceId}");
|
||||
p.Format.SpaceAfter = cm(0.5);
|
||||
}
|
||||
|
||||
// Provision location
|
||||
if (inv.ProvisionLocation.Length > 0)
|
||||
{
|
||||
var p = sec.AddParagraph();
|
||||
p.Style = "AddressBox";
|
||||
p.Format.Font.Size = 10;
|
||||
p.Format.LineSpacingRule = LineSpacingRule.Exactly;
|
||||
p.Format.LineSpacing = 12;
|
||||
p.AddFormattedText(tb.ProvisionLoc_Label, TextFormat.Bold);
|
||||
p.AddLineBreak();
|
||||
foreach (string t in inv.ProvisionLocation) { p.AddText(t); p.AddLineBreak(); }
|
||||
}
|
||||
sec.AddParagraph().Format.SpaceBefore = cm(0.7);
|
||||
|
||||
// Invoice items table
|
||||
var tbl = sec.AddTable();
|
||||
tbl.Style = "Table";
|
||||
tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1);
|
||||
tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25;
|
||||
tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5;
|
||||
tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6);
|
||||
tbl.Rows.HeightRule = RowHeightRule.AtLeast;
|
||||
|
||||
double pageW = sec.PageSetup.PageWidth.Centimeter
|
||||
- sec.PageSetup.LeftMargin.Centimeter
|
||||
- sec.PageSetup.RightMargin.Centimeter;
|
||||
double[] colWidths = { pageW * 0.06, pageW * 0.42, pageW * 0.12, pageW * 0.18, pageW * 0.22 };
|
||||
foreach (double w in colWidths) tbl.AddColumn(cm(w));
|
||||
|
||||
// Header row
|
||||
string[] headers = { "Pos.", "Bezeichnung", "Menge", "Einzelpreis", "Gesamtpreis" };
|
||||
var hRow = tbl.AddRow();
|
||||
hRow.Shading = GrayShading();
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
var p = hRow.Cells[i].AddParagraph(); p.Style = "TblCell_Head";
|
||||
p.AddFormattedText(headers[i], TextFormat.Bold);
|
||||
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
|
||||
}
|
||||
|
||||
// Data rows — grouped by service-request block (see FdsInvoiceData.InvoiceBlocks).
|
||||
// Each section prints its heading; positions are numbered the same way the editor numbers
|
||||
// them (every line except free-text/heading lines, including a set header). The chosen
|
||||
// set-display mode (see INVOICE_SET_PRICING.md) governs how "set" items and their members
|
||||
// are shown: SetPrice (default) prices the set, blanks members; ItemPrices prices the
|
||||
// members, blanks the set heading; SetOnly prices the set and drops the members entirely.
|
||||
// Blocks without any set items are unaffected and always render flat.
|
||||
var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions"));
|
||||
int pos = 0;
|
||||
foreach (var block in inv.InvoiceBlocks)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(block.Heading))
|
||||
{
|
||||
var hr = tbl.AddRow();
|
||||
hr.HeightRule = RowHeightRule.Auto;
|
||||
hr.Cells[1].MergeRight = 3; // span Bezeichnung … Gesamtpreis
|
||||
hr.Cells[1].AddParagraph().WithStyle("TblCell_RTitle").AddFormattedText(block.Heading, TextFormat.Bold);
|
||||
}
|
||||
|
||||
var lines = InvoiceSetPricing.ContainsSets(block.Items)
|
||||
? InvoiceSetPricing.Build(block.Items, setMode)
|
||||
: block.Items.Select(MapItemToLine).ToList(); // no sets: flat, faithful mirror of the editor
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
bool numbered = line.IsSetHeader || line.ShowPrice; // free-text/heading lines carry no number
|
||||
var row = tbl.AddRow();
|
||||
row.HeightRule = RowHeightRule.Auto;
|
||||
row.Cells[0].AddParagraph(numbered ? (++pos).ToString() : "").Style = "TblCell_Base";
|
||||
if (!string.IsNullOrEmpty(line.Title)) // skip the empty paragraph that added a blank line before free text
|
||||
{
|
||||
var titleCell = row.Cells[1].AddParagraph();
|
||||
titleCell.Style = "TblCell_RTitle";
|
||||
if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold);
|
||||
else titleCell.AddText(line.Title);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"<div>{line.Desc}</div>");
|
||||
row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base";
|
||||
row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base";
|
||||
row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.TotalNet) : "").Style = "TblCell_RSum";
|
||||
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
|
||||
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
|
||||
row.Cells[4].Format.Alignment = ParagraphAlignment.Right;
|
||||
}
|
||||
}
|
||||
|
||||
// Totals
|
||||
void TotalRow(string label, string value, string style = "TblCell_TSum", bool topBorder = false)
|
||||
{
|
||||
var r = tbl.AddRow(); r.Shading = LightShading();
|
||||
if (topBorder) r.Borders.Top.Width = 0.5;
|
||||
var lp = r.Cells[3].AddParagraph(); lp.Style = style; lp.AddText(label);
|
||||
r.Cells[3].Format.Alignment = ParagraphAlignment.Left;
|
||||
var vp = r.Cells[4].AddParagraph(); vp.Style = style; vp.AddText(value);
|
||||
r.Cells[4].Format.Alignment = ParagraphAlignment.Right;
|
||||
}
|
||||
|
||||
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance_net"), out decimal netSum);
|
||||
TotalRow("Nettobetrag:", Currency(netSum), "TblCell_TNet", topBorder: true);
|
||||
|
||||
// VAT rows
|
||||
foreach (var vr in inv.VatRows)
|
||||
{
|
||||
string pct = vr.Key;
|
||||
ParseDec(vr.Value.no("vat_amount", 0), out decimal vatAmt);
|
||||
TotalRow($"MwSt. {pct}%:", Currency(vatAmt), "TblCell_TVat");
|
||||
}
|
||||
|
||||
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal gross);
|
||||
TotalRow("Rechnungsbetrag:", Currency(gross), "TblCell_TSum");
|
||||
|
||||
|
||||
// ── Standard invoice notes (ported from legacy fuchs_fds_pdf.vb) ──────────
|
||||
foreach (var note in BuildInvoiceNotes(tb, inv, p13b))
|
||||
{
|
||||
var np = sec.AddParagraph(); np.Style = note.Style; np.AddText(note.Text);
|
||||
}
|
||||
|
||||
// GiroCode payment QR (only on finalized invoices with a positive balance)
|
||||
ParseDec(reg?.getItem("InvoiceBalance"), out decimal payAmount);
|
||||
ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal payAmount);
|
||||
if (!inv.IsDraft && payAmount > 0 && !string.IsNullOrWhiteSpace(inv.InvoiceId))
|
||||
AddGirocode(sec, payAmount, $"{inv.InvoiceTitle.ne("Rechnung")} {inv.InvoiceId}");
|
||||
}
|
||||
@@ -795,31 +861,37 @@ public static class FuchsPdf
|
||||
p.AddText(rem.ReminderTitle.ne("Zahlungserinnerung"));
|
||||
}
|
||||
|
||||
var (introLines, closingLines) = BuildReminderNotes(tb, rem);
|
||||
|
||||
// Opening text
|
||||
if (tb.ReminderTexts_before.TryGetValue(rtype, out var intro))
|
||||
if (introLines.Length > 0)
|
||||
{
|
||||
var p = sec.AddParagraph(); p.Style = "BodyText";
|
||||
p.Format.SpaceBefore = cm(0.5);
|
||||
foreach (string line in intro) { p.AddText(line); p.AddLineBreak(); }
|
||||
foreach (string line in introLines) { p.AddText(line); p.AddLineBreak(); }
|
||||
}
|
||||
sec.AddParagraph().Format.SpaceBefore = cm(0.7);
|
||||
|
||||
// Reminder items table
|
||||
// Reminder items table — mirrors the online editor's remHR columns exactly
|
||||
// (Rechnung, vom, Rechnungsbetrag, bereits bezahlt, noch offen).
|
||||
var tbl = sec.AddTable();
|
||||
tbl.Style = "Table";
|
||||
tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1);
|
||||
tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25;
|
||||
tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5;
|
||||
tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6);
|
||||
tbl.Rows.HeightRule = RowHeightRule.AtLeast;
|
||||
tbl.Format.SpaceBefore = mm(4);
|
||||
|
||||
double pageW = sec.PageSetup.PageWidth.Centimeter
|
||||
- sec.PageSetup.LeftMargin.Centimeter
|
||||
- sec.PageSetup.RightMargin.Centimeter;
|
||||
tbl.AddColumn(cm(pageW * 0.20));
|
||||
tbl.AddColumn(cm(pageW * 0.15));
|
||||
tbl.AddColumn(cm(pageW * 0.45));
|
||||
tbl.AddColumn(cm(pageW * 0.20));
|
||||
tbl.AddColumn(cm(pageW * 0.20));
|
||||
tbl.AddColumn(cm(pageW * 0.22));
|
||||
tbl.AddColumn(cm(pageW * 0.21));
|
||||
tbl.AddColumn(cm(pageW * 0.22));
|
||||
|
||||
string[] headers = { "Datum", "Bezeichnung", "Betrag", "Offen" };
|
||||
string[] headers = { "Rechnung", "vom", "Rechnungsbetrag", "bereits bezahlt", "noch offen" };
|
||||
var hRow = tbl.AddRow(); hRow.Shading = GrayShading();
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
@@ -828,33 +900,39 @@ public static class FuchsPdf
|
||||
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
|
||||
}
|
||||
|
||||
foreach (var itm in rem.ReminderItems)
|
||||
{
|
||||
var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto;
|
||||
row.Cells[0].AddParagraph(itm.nz("InvoiceDate", ""));
|
||||
row.Cells[1].AddParagraph(itm.nz("DocumentName", "").ne(itm.nz("InvoiceTitle", "")));
|
||||
row.Cells[2].AddParagraph(Currency(itm.no("InvoiceBalance", 0)));
|
||||
row.Cells[3].AddParagraph(Currency(itm.no("amount_open", 0)));
|
||||
row.Cells[2].Format.Alignment = ParagraphAlignment.Right;
|
||||
row.Cells[3].Format.Alignment = ParagraphAlignment.Right;
|
||||
}
|
||||
ParseDec(rem.ReminderRegistration?.getItem("amount"), out decimal amountTotal);
|
||||
ParseDec(rem.ReminderRegistration?.getItem("amount_payed"), out decimal amountPayed);
|
||||
ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal);
|
||||
string invoiceDate = rem.ReminderRegistration?.getString("InvoiceDate") is { Length: > 0 } d
|
||||
&& DateTime.TryParse(d, out var dd) ? dd.ToString("dd.MM.yy") : "";
|
||||
|
||||
var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto;
|
||||
row.Cells[0].AddParagraph(rem.InvoiceId);
|
||||
row.Cells[1].AddParagraph(invoiceDate);
|
||||
row.Cells[2].AddParagraph(Currency(amountTotal));
|
||||
row.Cells[3].AddParagraph(Currency(amountPayed));
|
||||
row.Cells[4].AddParagraph(Currency(openTotal));
|
||||
for (int i = 2; i <= 4; i++) row.Cells[i].Format.Alignment = ParagraphAlignment.Right;
|
||||
|
||||
// Open total
|
||||
ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal);
|
||||
var sumRow = tbl.AddRow(); sumRow.Shading = LightShading();
|
||||
sumRow.Borders.Top.Width = 0.5;
|
||||
sumRow.Cells[2].AddParagraph().AddFormattedText("Offener Betrag:", TextFormat.Bold);
|
||||
var sv = sumRow.Cells[3].AddParagraph();
|
||||
sumRow.Cells[3].AddParagraph().AddFormattedText("Offener Betrag:", TextFormat.Bold);
|
||||
var sv = sumRow.Cells[4].AddParagraph();
|
||||
sv.AddFormattedText(Currency(openTotal), TextFormat.Bold);
|
||||
sumRow.Cells[3].Format.Alignment = ParagraphAlignment.Right;
|
||||
sumRow.Cells[4].Format.Alignment = ParagraphAlignment.Right;
|
||||
|
||||
// Closing text + QR code
|
||||
var closing = sec.AddParagraph(); closing.Style = "BodyText";
|
||||
closing.Format.SpaceBefore = cm(0.8);
|
||||
string terms = TranslatePaymentTerm(rem.ReminderRegistration?.getString("PaymentTerm") ?? "");
|
||||
closing.AddText(
|
||||
$"Bitte \u00fcberweisen Sie den offenen Betrag von {Currency(openTotal)} innerhalb von {terms} auf unser Konto:\n" +
|
||||
"IBAN: DE76\u00a03005\u00a00110\u00a00045\u00a00148\u00a000, BIC DUSSSDEDDXXX (Stadtsparkasse D\u00fcsseldorf)");
|
||||
if (closingLines.Length > 0)
|
||||
{
|
||||
var closing = sec.AddParagraph(); closing.Style = "BodyText";
|
||||
closing.Format.SpaceBefore = cm(0.8);
|
||||
for (int i = 0; i < closingLines.Length; i++)
|
||||
{
|
||||
if (i > 0) closing.AddLineBreak();
|
||||
closing.AddText(closingLines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// GiroCode payment QR (only on finalized reminders with a positive open amount)
|
||||
if (!rem.IsDraft && openTotal > 0 && !string.IsNullOrWhiteSpace(rem.InvoiceId))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using static OCORE.commons;
|
||||
using static OCORE.commons;
|
||||
using static OCORE.OCORE_dictionaries;
|
||||
|
||||
namespace Fuchs.intranet;
|
||||
@@ -22,6 +22,9 @@ public enum SetDisplayMode
|
||||
/// <summary>A resolved invoice display line (after applying the set display mode).</summary>
|
||||
public sealed class InvoiceSetLine
|
||||
{
|
||||
/// <summary>The source item's id — lets callers (e.g. the online editor) map a resolved
|
||||
/// display line back to the DOM row it should update.</summary>
|
||||
public string Id { get; init; } = "";
|
||||
public string Title { get; init; } = "";
|
||||
public string Desc { get; init; } = "";
|
||||
public string Qty { get; init; } = "1";
|
||||
@@ -156,6 +159,7 @@ public static class InvoiceSetPricing
|
||||
|
||||
private static InvoiceSetLine HeaderLine(Dictionary<string, object?> i, decimal setTotal, bool showPrice) => new()
|
||||
{
|
||||
Id = i.nz("id", ""),
|
||||
Title = i.nz("title", ""),
|
||||
Desc = i.nz("desc", ""),
|
||||
Qty = i.nz("qty", "1"),
|
||||
@@ -171,6 +175,7 @@ public static class InvoiceSetPricing
|
||||
FuchsPdf.ParseDec(i.no("total_net", 0), out decimal total);
|
||||
return new InvoiceSetLine
|
||||
{
|
||||
Id = i.nz("id", ""),
|
||||
Title = i.nz("title", ""),
|
||||
Desc = i.nz("desc", ""),
|
||||
Qty = i.nz("qty", "1"),
|
||||
|
||||
Reference in New Issue
Block a user