Load purchasing EKPO and EKET metrics

This commit is contained in:
2026-06-05 11:05:04 +02:00
parent e7e408fc20
commit 2fa410ec31
5 changed files with 239 additions and 30 deletions
@@ -360,9 +360,9 @@
private IReadOnlyList<PurchasingKpiCard> KpiCards => private IReadOnlyList<PurchasingKpiCard> KpiCards =>
[ [
new("Spend total", "Total spend", _liveState.EkpoLoaded ? "EKPO live" : T("wartet auf EKPO", "waiting for EKPO"), _liveState.EkpoLoaded ? "Positionsdaten verfuegbar" : "EKKO live, Positionswerte fehlen noch", _liveState.EkpoLoaded ? "Position data available" : "EKKO live, position values still missing", Icons.Material.Filled.Payments, Color.Primary), new("Spend total", "Total spend", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : T("wartet auf EKPO", "waiting for EKPO"), _liveState.EkpoLoaded ? "EKPO-Live-Sample" : "EKKO live, Positionswerte fehlen noch", _liveState.EkpoLoaded ? "EKPO live sample" : "EKKO live, position values still missing", Icons.Material.Filled.Payments, Color.Primary),
new("Offene Bestellungen", "Open orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", _liveState.EkkoLoaded ? "EKKO-Belege seit Jahresbeginn" : "Noch nicht geladen", _liveState.EkkoLoaded ? "EKKO orders since start of year" : "Not loaded yet", Icons.Material.Filled.PendingActions, Color.Warning), new("Offene Bestellungen", "Open orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", _liveState.EkkoLoaded ? "EKKO-Belege seit Jahresbeginn" : "Noch nicht geladen", _liveState.EkkoLoaded ? "EKKO orders since start of year" : "Not loaded yet", Icons.Material.Filled.PendingActions, Color.Warning),
new("Kontrakte", "Contracts", _liveState.EketLoaded ? _liveState.ScheduleSampleCount.ToString("N0") : T("wartet auf EKET", "waiting for EKET"), _liveState.EketLoaded ? "Termin-/Einteilungsprobe verfuegbar" : "EKKO live, Terminwerte fehlen noch", _liveState.EketLoaded ? "Schedule sample available" : "EKKO live, schedule values still missing", Icons.Material.Filled.Assignment, Color.Info), new("Kontrakte", "Contracts", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : T("wartet auf EKET", "waiting for EKET"), _liveState.EketLoaded ? "Restwert aus EKET/EKPO-Sample" : "EKKO live, Terminwerte fehlen noch", _liveState.EketLoaded ? "Remaining value from EKET/EKPO sample" : "EKKO live, schedule values still missing", Icons.Material.Filled.Assignment, Color.Info),
new("Lieferantenperformance", "Supplier performance", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", _liveState.EkkoLoaded ? "Lieferanten in EKKO-Liveprobe" : "Noch nicht geladen", _liveState.EkkoLoaded ? "Suppliers in EKKO live sample" : "Not loaded yet", Icons.Material.Filled.Verified, Color.Success) new("Lieferantenperformance", "Supplier performance", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", _liveState.EkkoLoaded ? "Lieferanten in EKKO-Liveprobe" : "Noch nicht geladen", _liveState.EkkoLoaded ? "Suppliers in EKKO live sample" : "Not loaded yet", Icons.Material.Filled.Verified, Color.Success)
]; ];
@@ -432,7 +432,7 @@
private IReadOnlyList<PurchasingSectionKpi> SpendKpis => private IReadOnlyList<PurchasingSectionKpi> SpendKpis =>
[ [
new("Spend CHF", "Spend CHF", _liveState.EkpoLoaded ? "EKPO live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers"), new("Spend CHF", "Spend CHF", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers"),
new("Jahre", "Years", "2024-2026", "aus PBIX-Struktur", "from PBIX structure"), new("Jahre", "Years", "2024-2026", "aus PBIX-Struktur", "from PBIX structure"),
new("Dimensionen", "Dimensions", "4", "Jahr, Lieferant, Warengruppe, Artikel", "year, supplier, material group, article"), new("Dimensionen", "Dimensions", "4", "Jahr, Lieferant, Warengruppe, Artikel", "year, supplier, material group, article"),
new("SAP Status", "SAP status", _liveState.EkpoLoaded ? "live" : "wartet", "EKPOSet fuer Spend notwendig", "EKPOSet required for spend") new("SAP Status", "SAP status", _liveState.EkpoLoaded ? "live" : "wartet", "EKPOSet fuer Spend notwendig", "EKPOSet required for spend")
@@ -442,13 +442,13 @@
[ [
new("Bestellungen", "Orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"), new("Bestellungen", "Orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
new("Lieferanten", "Suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "aus EKKO-Liveprobe", "from EKKO live sample"), new("Lieferanten", "Suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "aus EKKO-Liveprobe", "from EKKO live sample"),
new("Offener Wert", "Open value", _liveState.EkpoLoaded ? "EKPO live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers"), new("Offener Wert", "Open value", _liveState.EketLoaded ? FormatChf(_liveState.OpenValueSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), _liveState.EketLoaded ? "aus SAP Terminen/Positionen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules/items" : "simulation until EKET delivers"),
new("Offene Menge", "Open quantity", _liveState.EkpoLoaded ? "EKPO live" : Purchasing3dBaseRows.Sum(x => x.OpenQuantity).ToString("N0"), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers") new("Offene Menge", "Open quantity", _liveState.EketLoaded ? _liveState.OpenQuantitySample.ToString("N0") : Purchasing3dBaseRows.Sum(x => x.OpenQuantity).ToString("N0"), _liveState.EketLoaded ? "aus SAP Terminen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules" : "simulation until EKET delivers")
]; ];
private IReadOnlyList<PurchasingSectionKpi> ContractKpis => private IReadOnlyList<PurchasingSectionKpi> ContractKpis =>
[ [
new("Restwert", "Remaining value", _liveState.EketLoaded ? "EKET live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), _liveState.EketLoaded ? "aus SAP Einteilungen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules" : "simulation until EKET delivers"), new("Restwert", "Remaining value", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), _liveState.EketLoaded ? "aus SAP Einteilungen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules" : "simulation until EKET delivers"),
new("Einteilungen", "Schedules", _liveState.EketLoaded ? _liveState.ScheduleSampleCount.ToString("N0") : "-", "EKET-Probe", "EKET sample"), new("Einteilungen", "Schedules", _liveState.EketLoaded ? _liveState.ScheduleSampleCount.ToString("N0") : "-", "EKET-Probe", "EKET sample"),
new("Abrufquote", "Consumption", "offen", "braucht Kontrakt- und Abrufdaten", "needs contract and call-off data"), new("Abrufquote", "Consumption", "offen", "braucht Kontrakt- und Abrufdaten", "needs contract and call-off data"),
new("Faelligkeit", "Due date", _liveState.LatestOrderDate?.ToString("yyyy-MM-dd") ?? "-", "letztes bekanntes EKKO-Datum", "latest known EKKO date") new("Faelligkeit", "Due date", _liveState.LatestOrderDate?.ToString("yyyy-MM-dd") ?? "-", "letztes bekanntes EKKO-Datum", "latest known EKKO date")
@@ -458,7 +458,7 @@
[ [
new("Aktive Lieferanten", "Active suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"), new("Aktive Lieferanten", "Active suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
new("Performance Score", "Performance score", $"{Purchasing3dBaseRows.Average(x => x.SupplierScore):N1}%", "Simulation bis Bewertungsdaten kommen", "simulation until rating data arrives"), new("Performance Score", "Performance score", $"{Purchasing3dBaseRows.Average(x => x.SupplierScore):N1}%", "Simulation bis Bewertungsdaten kommen", "simulation until rating data arrives"),
new("Preisindikator", "Price indicator", _liveState.EkpoLoaded ? "EKPO live" : "wartet", "Netwr CHF/Stk braucht EKPO", "Netwr CHF/unit needs EKPO"), new("Preisindikator", "Price indicator", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : "wartet", "Netwr CHF/Stk braucht EKPO", "Netwr CHF/unit needs EKPO"),
new("Qualitaet", "Quality", "offen", "Reklamationsquelle noch nicht angebunden", "claim source not connected yet") new("Qualitaet", "Quality", "offen", "Reklamationsquelle noch nicht angebunden", "claim source not connected yet")
]; ];
@@ -527,7 +527,9 @@
]; ];
private IReadOnlyList<PurchasingSectionChartRow> SpendChartRows private IReadOnlyList<PurchasingSectionChartRow> SpendChartRows
=> BuildPurchasingChartRows(x => x.Spend, FormatChf); => _liveState.EkpoLoaded && _liveState.SpendChartRows.Count > 0
? BuildLiveChartRows(_liveState.SpendChartRows, FormatChf)
: BuildPurchasingChartRows(x => x.Spend, FormatChf);
private IReadOnlyList<PurchasingSectionChartRow> OpenOrderChartRows private IReadOnlyList<PurchasingSectionChartRow> OpenOrderChartRows
=> _liveState.EkkoLoaded => _liveState.EkkoLoaded
@@ -535,7 +537,9 @@
: BuildPurchasingChartRows(x => x.OpenValue, FormatChf); : BuildPurchasingChartRows(x => x.OpenValue, FormatChf);
private IReadOnlyList<PurchasingSectionChartRow> ContractChartRows private IReadOnlyList<PurchasingSectionChartRow> ContractChartRows
=> BuildPurchasingChartRows(x => x.ContractValue, FormatChf); => _liveState.EketLoaded && _liveState.ContractChartRows.Count > 0
? BuildLiveChartRows(_liveState.ContractChartRows, FormatChf)
: BuildPurchasingChartRows(x => x.ContractValue, FormatChf);
private IReadOnlyList<PurchasingSectionChartRow> SupplierChartRows private IReadOnlyList<PurchasingSectionChartRow> SupplierChartRows
=> BuildPurchasingChartRows(x => x.SupplierScore, value => $"{value:N1}%"); => BuildPurchasingChartRows(x => x.SupplierScore, value => $"{value:N1}%");
@@ -544,13 +548,13 @@
[ [
BuildStatus("EKKO Bestellkoepfe", "EKKO purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? $"{_liveState.PurchaseOrderCount:N0}" : "-"), BuildStatus("EKKO Bestellkoepfe", "EKKO purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? $"{_liveState.PurchaseOrderCount:N0}" : "-"),
BuildStatus("EKPO Positionen", "EKPO item rows", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? $"{_liveState.PositionSampleCount:N0}" : "0"), BuildStatus("EKPO Positionen", "EKPO item rows", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? $"{_liveState.PositionSampleCount:N0}" : "0"),
BuildStatus("Spend CHF", "Spend CHF", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "Simulation") BuildStatus("Spend CHF", "Spend CHF", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : "Simulation")
]; ];
private IReadOnlyList<PurchasingSectionStatusRow> OpenOrderStatusRows => private IReadOnlyList<PurchasingSectionStatusRow> OpenOrderStatusRows =>
[ [
BuildStatus("Bestellkoepfe", "Purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? "live" : "-"), BuildStatus("Bestellkoepfe", "Purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? "live" : "-"),
BuildStatus("Offene Werte", "Open values", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "wartet auf EKPO"), BuildStatus("Offene Werte", "Open values", _liveState.EketLoaded, _liveState.EketLoaded ? FormatChf(_liveState.OpenValueSample) : "wartet auf EKET"),
BuildStatus("Faelligkeiten", "Due dates", _liveState.EketLoaded, _liveState.EketLoaded ? "live" : "wartet auf EKET") BuildStatus("Faelligkeiten", "Due dates", _liveState.EketLoaded, _liveState.EketLoaded ? "live" : "wartet auf EKET")
]; ];
@@ -558,7 +562,7 @@
[ [
BuildStatus("Kontraktkopf/-position", "Contract header/item", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "wartet auf EKPO"), BuildStatus("Kontraktkopf/-position", "Contract header/item", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "wartet auf EKPO"),
BuildStatus("Einteilungen", "Schedules", _liveState.EketLoaded, _liveState.EketLoaded ? $"{_liveState.ScheduleSampleCount:N0}" : "0"), BuildStatus("Einteilungen", "Schedules", _liveState.EketLoaded, _liveState.EketLoaded ? $"{_liveState.ScheduleSampleCount:N0}" : "0"),
BuildStatus("Restverpflichtung", "Remaining commitment", _liveState.EkpoLoaded && _liveState.EketLoaded, _liveState.EkpoLoaded && _liveState.EketLoaded ? "live" : "Simulation") BuildStatus("Restverpflichtung", "Remaining commitment", _liveState.EkpoLoaded && _liveState.EketLoaded, _liveState.EkpoLoaded && _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : "Simulation")
]; ];
private IReadOnlyList<PurchasingSectionStatusRow> SupplierStatusRows => private IReadOnlyList<PurchasingSectionStatusRow> SupplierStatusRows =>
@@ -570,7 +574,7 @@
private IReadOnlyList<PurchasingSectionDetailRow> SpendDetailRows => private IReadOnlyList<PurchasingSectionDetailRow> SpendDetailRows =>
[ [
new("Spend nach Jahr", "Spend by year", _liveState.EkpoLoaded ? "SAP live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), "EKKOSet.Bedat Jahr", _liveState.EkpoLoaded ? "SAP live" : "Simulation"), new("Spend nach Jahr", "Spend by year", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), "EKKOSet.Bedat Jahr", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
new("Spend nach Lieferant", "Spend by supplier", TopSpendLabel, "Data.Name / EKKOSet.Lifnr", _liveState.EkpoLoaded ? "SAP live" : "Simulation"), new("Spend nach Lieferant", "Spend by supplier", TopSpendLabel, "Data.Name / EKKOSet.Lifnr", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
new("Spend nach Warengruppe", "Spend by material group", TopMaterialGroupLabel, "Data (2).Warengruppe", _liveState.EkpoLoaded ? "SAP live" : "Simulation"), new("Spend nach Warengruppe", "Spend by material group", TopMaterialGroupLabel, "Data (2).Warengruppe", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
new("Spend nach Artikel", "Spend by article", TopArticleLabel, "EKPOSet.Matnr / Txz01", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP") new("Spend nach Artikel", "Spend by article", TopArticleLabel, "EKPOSet.Matnr / Txz01", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP")
@@ -580,13 +584,13 @@
[ [
new("Bestellungen seit Jahresbeginn", "Orders since start of year", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKOSet.Bedat", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"), new("Bestellungen seit Jahresbeginn", "Orders since start of year", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKOSet.Bedat", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"),
new("Lieferanten mit Bestellung", "Suppliers with order", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKOSet.Lifnr", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"), new("Lieferanten mit Bestellung", "Suppliers with order", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKOSet.Lifnr", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"),
new("Offener Bestellwert", "Open order value", _liveState.EkpoLoaded ? "SAP live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), "EKPOSet.Netwr CHF", _liveState.EkpoLoaded ? "SAP live" : "Simulation"), new("Offener Bestellwert", "Open order value", _liveState.EketLoaded ? FormatChf(_liveState.OpenValueSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), "EKPO/EKET", _liveState.EketLoaded ? "SAP live" : "Simulation"),
new("Faelligkeiten", "Due dates", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet.Eindt", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP") new("Faelligkeiten", "Due dates", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet.Eindt", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
]; ];
private IReadOnlyList<PurchasingSectionDetailRow> ContractDetailRows => private IReadOnlyList<PurchasingSectionDetailRow> ContractDetailRows =>
[ [
new("Restverpflichtung", "Remaining commitment", _liveState.EketLoaded ? "SAP live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), "EKPO/EKET", _liveState.EketLoaded ? "SAP live" : "Simulation"), new("Restverpflichtung", "Remaining commitment", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), "EKPO/EKET", _liveState.EketLoaded ? "SAP live" : "Simulation"),
new("Mengenkontrakte", "Quantity contracts", _liveState.EkpoLoaded ? "SAP live" : "wartet auf EKPO", "EKPOSet.Menge", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP"), new("Mengenkontrakte", "Quantity contracts", _liveState.EkpoLoaded ? "SAP live" : "wartet auf EKPO", "EKPOSet.Menge", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP"),
new("Abrufquote", "Consumption rate", "offen", "Kontraktmenge / Abrufmenge", "Wartet auf SAP"), new("Abrufquote", "Consumption rate", "offen", "Kontraktmenge / Abrufmenge", "Wartet auf SAP"),
new("Faellige Verpflichtungen", "Due commitments", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP") new("Faellige Verpflichtungen", "Due commitments", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
@@ -614,9 +618,10 @@
private string T(string german, string english) => UiText.Text(german, english); private string T(string german, string english) => UiText.Text(german, english);
private static string FormatChf(double value) => $"CHF {value:N0}"; private static string FormatChf(double value) => $"CHF {value:N0}";
private string TopSpendLabel => BuildTopLabel(x => x.Spend, FormatChf); private static string FormatChf(decimal value) => $"CHF {value:N0}";
private string TopMaterialGroupLabel => BuildTopLabel(x => x.Spend, FormatChf, "Warengruppe"); private string TopSpendLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopSupplierLabel) ? _liveState.TopSupplierLabel : BuildTopLabel(x => x.Spend, FormatChf);
private string TopArticleLabel => BuildTopLabel(x => x.Spend, FormatChf, "Artikel"); private string TopMaterialGroupLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopMaterialGroupLabel) ? _liveState.TopMaterialGroupLabel : BuildTopLabel(x => x.Spend, FormatChf, "Warengruppe");
private string TopArticleLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopArticleLabel) ? _liveState.TopArticleLabel : BuildTopLabel(x => x.Spend, FormatChf, "Artikel");
private string PurchasingStatusText private string PurchasingStatusText
=> _liveLoading => _liveLoading
? T("SAP-Einkaufsdaten werden geladen...", "Loading SAP purchasing data...") ? T("SAP-Einkaufsdaten werden geladen...", "Loading SAP purchasing data...")
@@ -654,9 +659,23 @@
.ToList(); .ToList();
} }
private IReadOnlyList<PurchasingSectionChartRow> BuildLiveChartRows(IReadOnlyList<PurchasingLiveChartPoint> rows, Func<decimal, string> formatter)
{
var max = rows.Count == 0 ? 0m : rows.Max(row => row.Value);
return rows
.Select((row, index) => new PurchasingSectionChartRow(
row.Label,
formatter(row.Value),
max <= 0 ? 0 : (double)(row.Value / max * 100m),
PurchasingPalette[index % PurchasingPalette.Length]))
.ToList();
}
private IReadOnlyList<PurchasingSectionChartRow> BuildOpenOrderLiveChartRows() private IReadOnlyList<PurchasingSectionChartRow> BuildOpenOrderLiveChartRows()
{ {
var simulatedRows = BuildPurchasingChartRows(x => x.OpenValue, FormatChf).ToList(); var simulatedRows = BuildPurchasingChartRows(x => x.OpenValue, FormatChf).ToList();
if (_liveState.EketLoaded && _liveState.OpenValueChartRows.Count > 0)
simulatedRows = BuildLiveChartRows(_liveState.OpenValueChartRows, FormatChf).ToList();
simulatedRows.Insert(0, new PurchasingSectionChartRow(T("EKKO Bestellungen live", "EKKO orders live"), _liveState.PurchaseOrderCount.ToString("N0"), 100d, "#2e7d32")); simulatedRows.Insert(0, new PurchasingSectionChartRow(T("EKKO Bestellungen live", "EKKO orders live"), _liveState.PurchaseOrderCount.ToString("N0"), 100d, "#2e7d32"));
simulatedRows.Insert(1, new PurchasingSectionChartRow(T("Lieferanten live", "Suppliers live"), _liveState.SupplierCount.ToString("N0"), _liveState.PurchaseOrderCount <= 0 ? 0 : Math.Min(100d, _liveState.SupplierCount / (double)_liveState.PurchaseOrderCount * 100d), "#1565c0")); simulatedRows.Insert(1, new PurchasingSectionChartRow(T("Lieferanten live", "Suppliers live"), _liveState.SupplierCount.ToString("N0"), _liveState.PurchaseOrderCount <= 0 ? 0 : Math.Min(100d, _liveState.SupplierCount / (double)_liveState.PurchaseOrderCount * 100d), "#1565c0"));
return simulatedRows.Take(6).ToList(); return simulatedRows.Take(6).ToList();
@@ -16,5 +16,17 @@ public sealed class PurchasingDashboardLiveState
public DateTime? LatestOrderDate { get; set; } public DateTime? LatestOrderDate { get; set; }
public int PositionSampleCount { get; set; } public int PositionSampleCount { get; set; }
public int ScheduleSampleCount { get; set; } public int ScheduleSampleCount { get; set; }
public decimal SpendChfSample { get; set; }
public decimal OpenQuantitySample { get; set; }
public decimal OpenValueSample { get; set; }
public decimal ContractValueSample { get; set; }
public string TopSupplierLabel { get; set; } = string.Empty;
public string TopMaterialGroupLabel { get; set; } = string.Empty;
public string TopArticleLabel { get; set; } = string.Empty;
public List<PurchasingLiveChartPoint> SpendChartRows { get; set; } = [];
public List<PurchasingLiveChartPoint> OpenValueChartRows { get; set; } = [];
public List<PurchasingLiveChartPoint> ContractChartRows { get; set; } = [];
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
} }
public sealed record PurchasingLiveChartPoint(string Label, decimal Value);
@@ -74,21 +74,26 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
{ {
var ekpoRows = await ReadRowsAsync( var ekpoRows = await ReadRowsAsync(
client, client,
$"{baseUrl}EKPOSet?$format=json&$top=50&$filter={Uri.EscapeDataString($"Ebeln eq '{firstEbeln}'")}", $"{baseUrl}EKPOSet?$format=json&$top=1000&$filter={Uri.EscapeDataString($"Ebeln ge '{firstEbeln}'")}",
cancellationToken); cancellationToken);
state.PositionSampleCount = ekpoRows.Count; state.PositionSampleCount = ekpoRows.Count;
state.EkpoLoaded = ekpoRows.Count > 0; state.EkpoLoaded = ekpoRows.Count > 0;
var eketRows = await ReadRowsAsync( var eketRows = await ReadRowsAsync(
client, client,
$"{baseUrl}eketSet?$format=json&$top=50&$filter={Uri.EscapeDataString($"Ebeln eq '{firstEbeln}'")}", $"{baseUrl}eketSet?$format=json&$top=1000&$filter={Uri.EscapeDataString($"Ebeln ge '{firstEbeln}'")}",
cancellationToken); cancellationToken);
state.ScheduleSampleCount = eketRows.Count; state.ScheduleSampleCount = eketRows.Count;
state.EketLoaded = eketRows.Count > 0; state.EketLoaded = eketRows.Count > 0;
ApplyEkpoMetrics(state, ekkoRows, ekpoRows);
ApplyEketMetrics(state, ekpoRows, eketRows);
} }
state.Message = state.EkpoLoaded state.Message = state.EkpoLoaded && state.EketLoaded
? "SAP Einkaufsdaten geladen." ? "SAP Einkaufsdaten inkl. EKPO/EKET geladen."
: state.EkpoLoaded
? "SAP Einkaufsdaten inkl. EKPO geladen; EKET liefert noch keine Termindaten."
: "EKKO ist live geladen; EKPO/EKET liefern aktuell noch keine Positionsdaten."; : "EKKO ist live geladen; EKPO/EKET liefern aktuell noch keine Positionsdaten.";
} }
catch (Exception ex) catch (Exception ex)
@@ -99,6 +104,105 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
return state; return state;
} }
private static void ApplyEkpoMetrics(
PurchasingDashboardLiveState state,
List<Dictionary<string, object?>> ekkoRows,
List<Dictionary<string, object?>> ekpoRows)
{
if (ekpoRows.Count == 0)
return;
var supplierByEbeln = ekkoRows
.Select(row => new { Ebeln = GetText(row, "Ebeln"), Lifnr = GetText(row, "Lifnr") })
.Where(row => !string.IsNullOrWhiteSpace(row.Ebeln))
.GroupBy(row => row.Ebeln, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First().Lifnr, StringComparer.OrdinalIgnoreCase);
var enriched = ekpoRows
.Select(row =>
{
var ebeln = GetText(row, "Ebeln");
supplierByEbeln.TryGetValue(ebeln, out var supplier);
var netwr = GetDecimal(row, "Netwr");
var quantity = GetDecimal(row, "Menge");
return new
{
Ebeln = ebeln,
Supplier = string.IsNullOrWhiteSpace(supplier) ? "ohne Lieferant" : supplier,
Material = FirstNonEmpty(GetText(row, "Matnr"), GetText(row, "Txz01"), "ohne Artikel"),
MaterialGroup = FirstNonEmpty(GetText(row, "Matkl"), "ohne Warengruppe"),
NetValue = netwr,
Quantity = quantity
};
})
.ToList();
state.SpendChfSample = enriched.Sum(row => row.NetValue);
state.TopSupplierLabel = BuildTopLabel(enriched.GroupBy(row => row.Supplier), row => row.NetValue, "Lieferant");
state.TopMaterialGroupLabel = BuildTopLabel(enriched.GroupBy(row => row.MaterialGroup), row => row.NetValue, "Warengruppe");
state.TopArticleLabel = BuildTopLabel(enriched.GroupBy(row => row.Material), row => row.NetValue, "Artikel");
state.SpendChartRows = enriched
.GroupBy(row => row.Supplier)
.Select(group => new PurchasingLiveChartPoint($"Lief. {group.Key}", group.Sum(row => row.NetValue)))
.OrderByDescending(row => row.Value)
.Take(6)
.ToList();
}
private static void ApplyEketMetrics(
PurchasingDashboardLiveState state,
List<Dictionary<string, object?>> ekpoRows,
List<Dictionary<string, object?>> eketRows)
{
if (eketRows.Count == 0)
return;
var netPriceByPosition = ekpoRows
.Select(row =>
{
var ebeln = GetText(row, "Ebeln");
var ebelp = GetText(row, "Ebelp");
var key = $"{ebeln}|{ebelp}";
var quantity = GetDecimal(row, "Menge");
var netValue = GetDecimal(row, "Netwr");
var netPrice = quantity == 0 ? 0 : netValue / quantity;
return new { key, netPrice };
})
.GroupBy(row => row.key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First().netPrice, StringComparer.OrdinalIgnoreCase);
var enriched = eketRows
.Select(row =>
{
var ebeln = GetText(row, "Ebeln");
var ebelp = GetText(row, "Ebelp");
var key = $"{ebeln}|{ebelp}";
netPriceByPosition.TryGetValue(key, out var netPrice);
var quantity = GetDecimal(row, "Menge");
var received = GetDecimal(row, "Wemng");
var openQuantity = Math.Max(0, quantity - received);
return new
{
Ebeln = ebeln,
DueDate = TryParseSapDate(GetText(row, "Eindt")),
OpenQuantity = openQuantity,
OpenValue = openQuantity * netPrice
};
})
.ToList();
state.OpenQuantitySample = enriched.Sum(row => row.OpenQuantity);
state.OpenValueSample = enriched.Sum(row => row.OpenValue);
state.ContractValueSample = state.OpenValueSample;
state.OpenValueChartRows = enriched
.GroupBy(row => row.DueDate?.ToString("yyyy-MM") ?? "ohne Termin")
.Select(group => new PurchasingLiveChartPoint(group.Key, group.Sum(row => row.OpenValue)))
.OrderBy(row => row.Label)
.Take(6)
.ToList();
state.ContractChartRows = state.OpenValueChartRows.ToList();
}
private static HttpClient CreateClient(string username, string password) private static HttpClient CreateClient(string username, string password)
{ {
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(45) }; var client = new HttpClient { Timeout = TimeSpan.FromSeconds(45) };
@@ -151,6 +255,27 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
private static string GetText(Dictionary<string, object?> row, string key) private static string GetText(Dictionary<string, object?> row, string key)
=> row.TryGetValue(key, out var value) ? Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty : string.Empty; => row.TryGetValue(key, out var value) ? Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty : string.Empty;
private static decimal GetDecimal(Dictionary<string, object?> row, string key)
{
var text = GetText(row, key);
return decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out var value)
|| decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out value)
? value
: 0m;
}
private static string FirstNonEmpty(params string[] values)
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
private static string BuildTopLabel<T>(IEnumerable<IGrouping<string, T>> groups, Func<T, decimal> selector, string fallback)
{
var top = groups
.Select(group => new { Label = group.Key, Value = group.Sum(selector) })
.OrderByDescending(row => row.Value)
.FirstOrDefault();
return top is null ? fallback : $"{top.Label}: CHF {top.Value:N0}";
}
private static DateTime? TryParseSapDate(string value) private static DateTime? TryParseSapDate(string value)
{ {
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed)) if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed))
@@ -47,9 +47,11 @@ Das Dashboard wurde fachlich um diese Bereiche erweitert:
- `Lieferanten` - `Lieferanten`
- `PBIX Vorlage` - `PBIX Vorlage`
- `3D Simulation` - `3D Simulation`
- `Ideen`
- Unterpunkt `Einkauf > Datenquellen` fuer SAP/OData-Verbindung, Quellen, Join-Fluss und Zielmappings. - Unterpunkt `Einkauf > Datenquellen` fuer SAP/OData-Verbindung, Quellen, Join-Fluss und Zielmappings.
- Die Seite ist als Cockpit-Struktur umgesetzt und zweisprachig ueber den vorhandenen UI-Sprachservice vorbereitet. - Die Seite ist als Cockpit-Struktur umgesetzt und zweisprachig ueber den vorhandenen UI-Sprachservice vorbereitet.
- Die Kennzahlen sind noch nicht live an SAP gebunden. - EKKO, EKPO und EKET werden live ueber SAP/OData gelesen.
- Die Kennzahlen im Cockpit nutzen aktuell eine begrenzte Live-Probe, damit das Dashboard sofort echte Einkaufsdaten zeigt.
## SAP/OData-Konfiguration ## SAP/OData-Konfiguration
@@ -70,6 +72,44 @@ Vorbefuellte Joins:
Die Seite verwendet dieselben Grundtabellen wie die Finance-/Standorte-Quellenpflege: `Sites`, `SapSourceDefinitions`, `SapJoinDefinitions`, `SapFieldMappings`. Die Seite verwendet dieselben Grundtabellen wie die Finance-/Standorte-Quellenpflege: `Sites`, `SapSourceDefinitions`, `SapJoinDefinitions`, `SapFieldMappings`.
## SAP/OData Live-Stand 2026-06-05
Der SAP-Test hat bestaetigt, dass die Einkaufstabellen Daten enthalten:
- `EKKO` ab `01.01.2026`: 2'748 Koepfe.
- `EKPO` gesamt: 233'920 Positionen.
- `EKET` gesamt: 242'571 Einteilungen.
- Join `EKKO -> EKPO` ab `01.01.2026`: 3'464 Zeilen.
- Join `EKKO -> EKET` ab `01.01.2026`: 3'458 Zeilen.
Nach Aktivierung der angepassten SAP-Methoden liefern die OData-Services:
- `EKPOSet?$top=5`: HTTP 200 mit Daten.
- `eketSet?$top=5`: HTTP 200 mit Daten.
- `EKPOSet?$filter=Ebeln eq '45148366'`: 1 Zeile.
- `eketSet?$filter=Ebeln eq '45148366'`: 1 Zeile.
Wichtig: Die OData-Property heisst `Ebeln`. Ein Filter mit `EBELN` liefert HTTP 400.
## Live-Kennzahlen im Dashboard
Die Seite `/einkauf` zeigt nun echte Werte aus SAP:
- `Spend total`: Summe `EKPOSet.Netwr` aus der Live-Probe.
- `Offene Bestellungen`: Anzahl EKKO-Belege seit Jahresbeginn.
- `Kontrakte`: offener Restwert aus `EKET.Menge - EKET.Wemng` bewertet mit EKPO-Netto-Stueckwert.
- `Offener Bestellwert`: berechnet aus EKET-Offenmenge und EKPO-Netto-Stueckwert.
- `Offene Menge`: Summe offener EKET-Mengen.
- Top-Lieferant, Top-Warengruppe und Top-Artikel werden aus EKPO gruppiert.
- Spend-, Offenwert- und Kontrakt-Diagramme verwenden Live-Gruppierungen, sofern EKPO/EKET Daten liefern.
Aktuelle technische Begrenzung:
- Das Dashboard laedt fuer EKPO/EKET eine begrenzte Probe mit `$top=1000`.
- Filter ist `Ebeln ge <erste aktuelle EKKO-Bestellnummer>`.
- Damit sind die Werte echte SAP-Werte, aber noch keine vollstaendige Jahresaggregation.
- Fuer definitive Management-Summen braucht es als naechsten Schritt serverseitige OData-Filter/Aggregation oder einen eigenen Import-/Cache-Prozess analog Finance.
## 3D Simulation ## 3D Simulation
Das Einkaufsdashboard hat eine eigene 3D-Simulation fuer wichtige Einkaufsindikatoren: Das Einkaufsdashboard hat eine eigene 3D-Simulation fuer wichtige Einkaufsindikatoren:
@@ -84,12 +124,25 @@ Die Simulation nutzt feste Canvas-Groessen, sichtbare Achsen, waehlbare Diagramm
## Naechster Schritt fuer Live-Daten ## Naechster Schritt fuer Live-Daten
Fuer echte Werte muessen die Einkaufsquellen sauber gemappt werden: Fuer definitive Vollwerte muessen die Live-Quellen noch fachlich fertig aggregiert werden:
- Bestellkopf, z. B. `EKKOSet`. - Jahres-/Periodenfilter fuer `EKKOSet.Bedat`.
- Bestellpositionen, z. B. `EKPOSet`. - Vollstaendige Aggregation von `EKPOSet.Netwr` nach Jahr, Lieferant, Warengruppe und Artikel.
- Offene Liefer-/Terminmengen, voraussichtlich Termin-/Schedule-Daten. - Vollstaendige offene Werte/Mengen aus `EKET` und `EKPO`.
- Kontrakte und offene Verpflichtungen. - Kontrakte und offene Verpflichtungen, inkl. fachlicher Abgrenzung von normalen Bestellungen.
- Lieferantenbewertung / Performance, falls im SAP-System als OData- oder HANA-Quelle verfuegbar. - Lieferantenbewertung / Performance, falls im SAP-System als OData- oder HANA-Quelle verfuegbar.
Danach koennen Filter, Aggregationen und Delta-/Refresh-Prozess analog zu Finance/Spain umgesetzt werden. Danach koennen Filter, Aggregationen und Delta-/Refresh-Prozess analog zu Finance/Spain umgesetzt werden.
## Geaenderte Programmstellen
- `Components/Pages/PurchasingDashboard.razor`
- KPI-Karten, Detailtabellen und Diagramme lesen jetzt Live-Werte aus `PurchasingDashboardLiveState`.
- Fallback-Simulation bleibt sichtbar, falls SAP/OData nicht antwortet.
- `Services/IPurchasingDashboardService.cs`
- Live-State um Spend, offene Menge, offenen Wert, Kontraktwert und Live-Diagrammzeilen erweitert.
- `Services/PurchasingDashboardService.cs`
- Laedt EKKO, EKPO und EKET.
- Berechnet Spend aus EKPO.
- Berechnet offene Mengen/Werte aus EKET minus Wareneingangsmenge, bewertet mit EKPO-Netto-Stueckwert.
- Erstellt Top-Gruppierungen fuer Lieferant, Warengruppe und Artikel.
+1 -1
View File
@@ -12,7 +12,7 @@ Stand: 2026-06-05
- Neu in der Navigation: Menuebaum wird aus `NavigationMenuItems` gerendert; Admins koennen bestehende Punkte unter `Admin > Menuestruktur` umhaengen, sortieren und aus-/einblenden. - Neu in der Navigation: Menuebaum wird aus `NavigationMenuItems` gerendert; Admins koennen bestehende Punkte unter `Admin > Menuestruktur` umhaengen, sortieren und aus-/einblenden.
- Neu als Hauptbereich: `Einkauf` mit Einkaufswagen-Icon und erweitertem `Einkauf Dashboard`. - Neu als Hauptbereich: `Einkauf` mit Einkaufswagen-Icon und erweitertem `Einkauf Dashboard`.
- Einkauf: `x.pbix` wurde als Vorlage analysiert; `/einkauf` enthaelt jetzt Struktur fuer Spend, offene Bestellungen, Mengenkontrakte, Lieferantenperformance, PBIX-Reportseiten und 3D-Simulation. - Einkauf: `x.pbix` wurde als Vorlage analysiert; `/einkauf` enthaelt jetzt Struktur fuer Spend, offene Bestellungen, Mengenkontrakte, Lieferantenperformance, PBIX-Reportseiten und 3D-Simulation.
- Einkauf: `Einkauf > Datenquellen` pflegt die SAP/OData-Konfiguration grafisch und ist mit `EKKOSet`, `EKPOSet`, `eketSet`, `Data`, `Data2`, Joins und Zielmappings vorbefuellt. Realer Kennzahlenimport ist noch offen. - Einkauf: `Einkauf > Datenquellen` pflegt die SAP/OData-Konfiguration grafisch und ist mit `EKKOSet`, `EKPOSet`, `eketSet`, `Data`, `Data2`, Joins und Zielmappings vorbefuellt. `/einkauf` laedt EKKO/EKPO/EKET live und zeigt eine echte, begrenzte SAP-Probe fuer Spend, offene Werte/Mengen und Kontrakt-Restwerte. Vollstaendige Jahresaggregation und Lieferantenperformance sind noch offen.
- Neu im Expertenbereich: `3D Datenanalyse` mit drehbarer 3D-Grafik, Achsen, Diagrammarten, Indikatorauswahl, Labelgroesse und Simulation per Schieberegler. - Neu im Expertenbereich: `3D Datenanalyse` mit drehbarer 3D-Grafik, Achsen, Diagrammarten, Indikatorauswahl, Labelgroesse und Simulation per Schieberegler.
- Spanien: `Run-SpainRangeExportAndUpload-AllInOne.ps1` exportiert Sage-Range direkt und laedt CSV/Summary via rclone nach SharePoint `trafag-bi:Import/Finance/Spanien`. - Spanien: `Run-SpainRangeExportAndUpload-AllInOne.ps1` exportiert Sage-Range direkt und laedt CSV/Summary via rclone nach SharePoint `trafag-bi:Import/Finance/Spanien`.
- Spanien: Default-Range ist heute minus 7 Tage bis heute; `ToDate` ist exklusiv. - Spanien: Default-Range ist heute minus 7 Tage bis heute; `ToDate` ist exklusiv.