Refactor code structure and remove redundant sections for improved readability and maintainability
This commit is contained in:
@@ -19,7 +19,7 @@ public class FdsInvoiceData
|
||||
public GenericObjectDictionary? Admin { get; private set; }
|
||||
public GenericObjectDictionary? NewValues { get; private set; }
|
||||
public GenericObjectDictionary? Sms { get; private set; }
|
||||
public List<Dictionary<string, object>>? Req { get; private set; }
|
||||
public List<Dictionary<string, object>>? Req { get; internal set; }
|
||||
|
||||
public GenericObjectDictionary? InvoiceRegistration { get; internal set; }
|
||||
public bool IsDraft { get; internal set; } = true;
|
||||
@@ -46,9 +46,25 @@ public class FdsInvoiceData
|
||||
get
|
||||
{
|
||||
var result = new List<Dictionary<string, object?>>();
|
||||
foreach (var block in InvoiceBlocks) result.AddRange(block.Items);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The service-request groups as they should render on the invoice: each block carries its
|
||||
/// heading (the section title the editor shows) and its line items. The PDF renders a heading
|
||||
/// row per block followed by that block's items, so the online editor and the PDF stay in sync.
|
||||
/// </summary>
|
||||
public List<InvoiceBlock> InvoiceBlocks
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = new List<InvoiceBlock>();
|
||||
if (Req == null) return result;
|
||||
foreach (var req in Req)
|
||||
{
|
||||
var items = new List<Dictionary<string, object?>>();
|
||||
if (req.TryGetValue("items", out var itmsObj))
|
||||
{
|
||||
IEnumerable<Dictionary<string, object?>>? itms =
|
||||
@@ -56,8 +72,12 @@ public class FdsInvoiceData
|
||||
?? (itmsObj is JArray ja
|
||||
? ja.ToObject<List<Dictionary<string, object?>>>()
|
||||
: null);
|
||||
if (itms != null) result.AddRange(itms);
|
||||
if (itms != null) items.AddRange(itms);
|
||||
}
|
||||
string heading = "";
|
||||
if (req.TryGetValue("text", out var th) && th != null) heading = th.ToString() ?? "";
|
||||
if (heading.Length == 0 && req.TryGetValue("nme", out var nh) && nh != null) heading = nh.ToString() ?? "";
|
||||
result.Add(new InvoiceBlock { Heading = heading, Items = items });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -196,3 +216,12 @@ public class FdsInvoiceData
|
||||
return double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A service-request group as it renders on the invoice: a heading plus its line items.</summary>
|
||||
public sealed class InvoiceBlock
|
||||
{
|
||||
/// <summary>The section heading (editor's <c>text</c>/<c>nme</c>); empty when the group has none.</summary>
|
||||
public string Heading { get; init; } = "";
|
||||
/// <summary>The group's line items (the editor's <c>items</c> contract).</summary>
|
||||
public List<Dictionary<string, object?>> Items { get; init; } = new();
|
||||
}
|
||||
|
||||
+65
-22
@@ -137,6 +137,30 @@ public static class FuchsPdf
|
||||
public static string TranslatePaymentTerm(string pt) =>
|
||||
pt.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen");
|
||||
|
||||
/// <summary>
|
||||
/// Maps one editor item-contract entry to a display line, mirroring the online editor: the
|
||||
/// item's own price/total is shown, a set header is emphasised, and free-text/heading lines
|
||||
/// (type <c>text</c>/<c>title</c>) show neither a price nor a position number. This is the flat
|
||||
/// (non-collapsing) rendering used for every set mode except the explicit <c>setonly</c>.
|
||||
/// </summary>
|
||||
private static InvoiceSetLine MapItemToLine(Dictionary<string, object?> i)
|
||||
{
|
||||
string type = i.nz("type", "").ToLowerInvariant();
|
||||
bool isText = type is "text" or "title";
|
||||
ParseDec(i.no("price_net", 0), out decimal price);
|
||||
ParseDec(i.no("total_net", 0), out decimal total);
|
||||
return new InvoiceSetLine
|
||||
{
|
||||
Title = i.nz("title", ""),
|
||||
Desc = i.nz("desc", ""),
|
||||
Qty = i.nz("qty", ""),
|
||||
PriceNet = price,
|
||||
TotalNet = total,
|
||||
ShowPrice = !isText,
|
||||
IsSetHeader = type == "set"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a numeric value coming from JSON deserialization (long/double), SQL
|
||||
/// (decimal), or an already-invariant numeric string. Numeric CLR types are
|
||||
@@ -593,30 +617,49 @@ public static class FuchsPdf
|
||||
hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left;
|
||||
}
|
||||
|
||||
// Data rows — resolved through the set-display mode (see InvoiceSetPricing).
|
||||
// For invoices without sets this passes items through unchanged; for sets it
|
||||
// emits set header + members per the chosen mode, blanking price cells where
|
||||
// a line should show no price. Totals come from the registration balance, so
|
||||
// the mode is purely presentational.
|
||||
// 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 = 1;
|
||||
foreach (var line in InvoiceSetPricing.Build(inv.InvoiceItems, setMode))
|
||||
int pos = 0;
|
||||
foreach (var block in inv.InvoiceBlocks)
|
||||
{
|
||||
var row = tbl.AddRow();
|
||||
row.HeightRule = RowHeightRule.Auto;
|
||||
row.Cells[0].AddParagraph(pos.ToString()).Style = "TblCell_Base";
|
||||
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;
|
||||
pos++;
|
||||
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
|
||||
|
||||
@@ -68,6 +68,31 @@ public static class InvoiceDraftCalculator
|
||||
session.Sums = sums;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renumbers the visible line positions authoritatively (the port of the client-side
|
||||
/// numbering in <c>invSumUpdate</c>): priced lines are numbered sequentially across the whole
|
||||
/// invoice — matching the PDF's <c>Pos.</c> column — while heading/free-text lines
|
||||
/// (<c>typ</c> = "text"/"title") carry no number. The result is written onto each line's
|
||||
/// <c>p</c> field so it flows back to the browser (via the view state) and into the PDF; this
|
||||
/// keeps the online editor and the PDF preview showing the same position numbers, including
|
||||
/// after a reorder.
|
||||
/// </summary>
|
||||
public static void RecomputePositions(InvoiceDraftSession session)
|
||||
{
|
||||
int pos = 0;
|
||||
foreach (var blockTok in session.Req)
|
||||
{
|
||||
if (blockTok is not JObject block || block["itm"] is not JArray lines) continue;
|
||||
foreach (var lineTok in lines)
|
||||
{
|
||||
if (lineTok is not JObject co) continue;
|
||||
string typ = Str(co["typ"]).Trim().ToLowerInvariant();
|
||||
bool numbered = typ is not ("text" or "title"); // only headings/free-text carry no number (mirrors invSumUpdate)
|
||||
co["p"] = numbered ? (JToken)(++pos) : (JToken)"";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the draft's plausibility / consistency findings. "error" severity marks
|
||||
/// issues that should block a clean finalise; "warning" is advisory. Kept in German,
|
||||
|
||||
Reference in New Issue
Block a user