Align purchasing analytics with Power BI

This commit is contained in:
2026-06-05 14:04:27 +02:00
parent aa6d0d0804
commit a41ef0a564
5 changed files with 100 additions and 92 deletions
@@ -800,7 +800,7 @@
private IReadOnlyList<PurchasingIdea> PurchasingIdeas => private IReadOnlyList<PurchasingIdea> PurchasingIdeas =>
[ [
new("Lieferantenrisiko", "Supplier risk", "Kombiniert Abhaengigkeit, Single-Source-Anteil, offene Bestellungen und Lieferperformance zu einem Risiko-Score.", "Combines dependency, single-source share, open orders and delivery performance into one risk score.", "EKKO, EKPO, EKET, LFA1", "Engpaesse und Lieferantenabhaengigkeit frueh sehen", "see shortages and supplier dependency early", _liveState.EkpoLoaded && _liveState.EketLoaded ? "berechenbar" : "wartet auf EKPO/EKET", _liveState.EkpoLoaded && _liveState.EketLoaded ? "calculable" : "waiting for EKPO/EKET", Icons.Material.Filled.WarningAmber, _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning), new("Lieferantenrisiko", "Supplier risk", "Kombiniert Abhaengigkeit, Single-Source-Anteil, offene Bestellungen und Lieferperformance zu einem Risiko-Score.", "Combines dependency, single-source share, open orders and delivery performance into one risk score.", "EKKO, EKPO, EKET, LFA1", "Engpaesse und Lieferantenabhaengigkeit frueh sehen", "see shortages and supplier dependency early", _liveState.EkpoLoaded && _liveState.EketLoaded ? "berechenbar" : "wartet auf EKPO/EKET", _liveState.EkpoLoaded && _liveState.EketLoaded ? "calculable" : "waiting for EKPO/EKET", Icons.Material.Filled.WarningAmber, _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning),
new("Preisabweichung", "Price variance", "Zeigt Preissteigerungen pro Artikel/Lieferant gegen Vorjahr, Budget oder letzten Einkaufspreis.", "Shows price increases by article/supplier against prior year, budget or last purchase price.", "EKPO, EKKO, FX", "Sparpotenziale und Ausreisser direkt sichtbar", "savings potential and outliers visible immediately", _liveState.EkpoLoaded ? "berechenbar" : "wartet auf EKPO", _liveState.EkpoLoaded ? "calculable" : "waiting for EKPO", Icons.Material.Filled.TrendingUp, _liveState.EkpoLoaded ? Color.Success : Color.Warning), new("Preisentwicklung CHF", "Price trend CHF", "Entspricht PowerBI: Minimum Netto-Stueckpreis pro Artikel und Jahr.", "Matches Power BI: minimum net unit price by article and year.", "EKPO, EKKO", "Artikelpreise wie in PowerBI pruefen", "check article prices like in Power BI", _liveState.EkpoLoaded ? "berechenbar" : "wartet auf EKPO", _liveState.EkpoLoaded ? "calculable" : "waiting for EKPO", Icons.Material.Filled.TrendingUp, _liveState.EkpoLoaded ? Color.Success : Color.Warning),
new("Maverick Buying", "Maverick buying", "Findet Bestellungen ausserhalb bevorzugter Lieferanten, Rahmenvertraege oder Warengruppenregeln.", "Finds orders outside preferred suppliers, contracts or material-group rules.", "EKKO, EKPO, Kontrakte", "Compliance und Buendelung verbessern", "improve compliance and bundling", "Konzept", "concept", Icons.Material.Filled.Policy, Color.Info), new("Maverick Buying", "Maverick buying", "Findet Bestellungen ausserhalb bevorzugter Lieferanten, Rahmenvertraege oder Warengruppenregeln.", "Finds orders outside preferred suppliers, contracts or material-group rules.", "EKKO, EKPO, Kontrakte", "Compliance und Buendelung verbessern", "improve compliance and bundling", "Konzept", "concept", Icons.Material.Filled.Policy, Color.Info),
new("Rahmenvertragsnutzung", "Contract utilisation", "Zeigt Kontraktmenge, Abrufmenge, Restmenge, Laufzeit und drohenden Verfall.", "Shows contract quantity, call-off quantity, remaining quantity, term and expiry risk.", "EKKO, EKPO, EKET", "Restverpflichtungen aktiv steuern", "actively manage remaining commitments", _liveState.EketLoaded ? "teilweise" : "wartet auf EKET", _liveState.EketLoaded ? "partial" : "waiting for EKET", Icons.Material.Filled.AssignmentTurnedIn, _liveState.EketLoaded ? Color.Success : Color.Warning), new("Rahmenvertragsnutzung", "Contract utilisation", "Zeigt Kontraktmenge, Abrufmenge, Restmenge, Laufzeit und drohenden Verfall.", "Shows contract quantity, call-off quantity, remaining quantity, term and expiry risk.", "EKKO, EKPO, EKET", "Restverpflichtungen aktiv steuern", "actively manage remaining commitments", _liveState.EketLoaded ? "teilweise" : "wartet auf EKET", _liveState.EketLoaded ? "partial" : "waiting for EKET", Icons.Material.Filled.AssignmentTurnedIn, _liveState.EketLoaded ? Color.Success : Color.Warning),
new("Working Capital", "Working capital", "Verbindet offene Bestellungen, Liefertermine und Zahlungs-/Bestandswirkung zu Cash-Ausblick.", "Connects open orders, delivery dates and payment/inventory impact into a cash outlook.", "EKPO, EKET, FI/AP", "Cashbedarf aus Einkauf vorhersagen", "forecast purchasing cash needs", "Konzept", "concept", Icons.Material.Filled.AccountBalanceWallet, Color.Info), new("Working Capital", "Working capital", "Verbindet offene Bestellungen, Liefertermine und Zahlungs-/Bestandswirkung zu Cash-Ausblick.", "Connects open orders, delivery dates and payment/inventory impact into a cash outlook.", "EKPO, EKET, FI/AP", "Cashbedarf aus Einkauf vorhersagen", "forecast purchasing cash needs", "Konzept", "concept", Icons.Material.Filled.AccountBalanceWallet, Color.Info),
@@ -814,7 +814,7 @@
private IReadOnlyList<PurchasingIdeaPriority> PurchasingIdeaPriorities => private IReadOnlyList<PurchasingIdeaPriority> PurchasingIdeaPriorities =>
[ [
new("1. Live-Probe zu Vollaggregation", "1. Live sample to full aggregation", "EKPO/EKET liefern Daten; jetzt braucht es saubere Jahres-/Periodenaggregation.", "EKPO/EKET now deliver data; next is clean year/period aggregation.", Icons.Material.Filled.BuildCircle, Color.Success), new("1. Live-Probe zu Vollaggregation", "1. Live sample to full aggregation", "EKPO/EKET liefern Daten; jetzt braucht es saubere Jahres-/Periodenaggregation.", "EKPO/EKET now deliver data; next is clean year/period aggregation.", Icons.Material.Filled.BuildCircle, Color.Success),
new("2. Preisabweichung aktivieren", "2. Activate price variance", "Hoher Nutzen, weil EKPO Werte, Artikel und Lieferanten jetzt verfuegbar sind.", "High value because EKPO values, articles and suppliers are now available.", Icons.Material.Filled.TrendingUp, Color.Info), new("2. Preisentwicklung aktivieren", "2. Activate price trend", "PowerBI nutzt Min(EKPO.Netwr CHF/Stk); diese Logik ist jetzt lokal nachgebaut.", "Power BI uses Min(EKPO.Netwr CHF/unit); this logic is now rebuilt locally.", Icons.Material.Filled.TrendingUp, Color.Info),
new("3. Liefertermin-Risiko anzeigen", "3. Show delivery due-date risk", "EKET offene Mengen und Termine sind die beste operative Fruehwarnung.", "EKET open quantities and dates are the best operational early warning.", Icons.Material.Filled.PendingActions, Color.Info), new("3. Liefertermin-Risiko anzeigen", "3. Show delivery due-date risk", "EKET offene Mengen und Termine sind die beste operative Fruehwarnung.", "EKET open quantities and dates are the best operational early warning.", Icons.Material.Filled.PendingActions, Color.Info),
new("4. Lieferantenrisiko aufbauen", "4. Build supplier risk", "Kombiniert Performance, offene Werte, Konzentration und Abhaengigkeit.", "Combines performance, open values, concentration and dependency.", Icons.Material.Filled.Security, Color.Info), new("4. Lieferantenrisiko aufbauen", "4. Build supplier risk", "Kombiniert Performance, offene Werte, Konzentration und Abhaengigkeit.", "Combines performance, open values, concentration and dependency.", Icons.Material.Filled.Security, Color.Info),
new("5. Contract Cockpit ausbauen", "5. Extend contract cockpit", "Mengenkontrakte und Restverpflichtungen brauchen klare fachliche Abgrenzung.", "Quantity contracts and remaining commitments need clear functional separation.", Icons.Material.Filled.Assignment, Color.Info) new("5. Contract Cockpit ausbauen", "5. Extend contract cockpit", "Mengenkontrakte und Restverpflichtungen brauchen klare fachliche Abgrenzung.", "Quantity contracts and remaining commitments need clear functional separation.", Icons.Material.Filled.Assignment, Color.Info)
@@ -876,27 +876,27 @@
]), ]),
new( new(
"ideen/preisabweichung", "ideen/preisabweichung",
"Preisabweichung", "Preisentwicklung CHF",
"Price variance", "Price trend CHF",
"Preisveraenderungen pro Artikel/Lieferant.", "PowerBI-Logik pro Artikel/Jahr.",
"Price changes by article/supplier.", "Power BI logic by article/year.",
"Preissteigerungen und Ausreisser werden gegen Vorjahr, letzte Bestellung oder Budget-/Referenzpreis sichtbar gemacht.", "Minimum Netto-Stueckpreis wird pro Artikel und Jahr sichtbar gemacht.",
"Price increases and outliers are shown against prior year, last order or budget/reference price.", "Minimum net unit price is shown by article and year.",
"EKPO.Netwr, EKPO.Menge, EKPO.Matnr, EKKO.Bedat, EKKO.Lifnr, FX/Budgetkurse", "EKPO.Netwr, EKPO.Menge, EKPO.Matnr, EKKO.Bedat, EKKO.Lifnr, FX/Budgetkurse",
"Preisdelta %, Preisdelta CHF, letzter Preis, Referenzpreis, potentieller Mehrpreis.", "Min(Netwr CHF/Stk), Artikel, Jahr, Lieferantenslicer.",
"price delta %, price delta CHF, last price, reference price, potential extra cost.", "Min(Netwr CHF/unit), article, year, supplier slicer.",
"Netto-Stueckpreis je Artikel/Lieferant/Periode bilden und gegen Referenzperiode vergleichen; Mengenwirkung separat ausweisen.", "Netto-Stueckpreis als Netwr/Menge bilden und Minimum je Artikel/Jahr ausweisen.",
"calculate net unit price by article/supplier/period and compare against reference period; show quantity effect separately.", "calculate net unit price as Netwr/quantity and show minimum by article/year.",
"Naechster Schritt: Referenzlogik festlegen: Vorjahr, letzter Preis oder Budgetpreis.", "Naechster Schritt: Lieferantennamen aus Data-Quelle mitcachen.",
"Next step: define reference logic: prior year, last price or budget price.", "Next step: cache supplier names from Data source.",
_liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO",
_liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO",
Icons.Material.Filled.TrendingUp, Icons.Material.Filled.TrendingUp,
_liveState.EkpoLoaded ? Color.Success : Color.Warning, _liveState.EkpoLoaded ? Color.Success : Color.Warning,
[ [
new("Preisnormalisierung", "Price normalisation", "Preis pro Einheit stabil aus NETWR/MENGE bilden.", "derive stable unit price from NETWR/MENGE."), new("Preisnormalisierung", "Price normalisation", "Preis pro Einheit stabil aus NETWR/MENGE bilden.", "derive stable unit price from NETWR/MENGE."),
new("Referenzperiode", "Reference period", "Vergleich gegen Vorjahr oder letzten Einkaufspreis waehlen.", "choose comparison against prior year or last purchase price."), new("Jahr", "Year", "PowerBI nutzt EKKO.Bedat Jahr als Spalte.", "Power BI uses EKKO.Bedat year as column."),
new("Ausreisser", "Outliers", "Top Preissteigerungen nach CHF-Wirkung und Prozent anzeigen.", "show top price increases by CHF impact and percent.") new("Minimum", "Minimum", "PowerBI aggregiert mit Min(Netwr CHF/Stk).", "Power BI aggregates with Min(Netwr CHF/unit).")
]), ]),
new( new(
"ideen/spend-konzentration", "ideen/spend-konzentration",
@@ -971,21 +971,21 @@
Icons.Material.Filled.WarningAmber, Icons.Material.Filled.WarningAmber,
_liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning), _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning),
new( new(
"Preisabweichung", "Preisentwicklung CHF",
"Price variance", "Price trend CHF",
"Preisveraenderungen pro Artikel und Lieferant.", "PowerBI-Preislogik nach Artikel und Jahr.",
"Price changes by article and supplier.", "Power BI price logic by article and year.",
"Preissteigerungen, Ausreisser und Verhandlungspotenzial transparent machen.", "Minimum Netto-Stueckpreis wie in PowerBI transparent machen.",
"Make price increases, outliers and negotiation potential transparent.", "Make minimum net unit price transparent like in Power BI.",
"EKPO, EKKO.Bedat, Waehrung/FX, Artikel, Lieferant", "EKPO.Netwr, EKPO.Menge, EKPO.Matnr, EKPO.Txz01, EKKO.Bedat",
"Preisdelta CHF, Preisdelta %, letzter Preis, Vorjahrespreis, Budgetpreis, Einsparpotenzial.", "Min(Netwr CHF/Stk), Artikel, Jahr, Lieferantenslicer.",
"price delta CHF, price delta %, last price, prior-year price, budget price, savings potential.", "Min(Netwr CHF/unit), article, year, supplier slicer.",
"Netto-Stueckpreis je Artikel/Lieferant/Periode bilden und gegen Referenzperiode oder Budgetkurs vergleichen.", "Stueckpreis = Netto / Menge, danach Minimum je Artikel/Jahr wie PowerBI.",
"Calculate net unit price by article/supplier/period and compare against reference period or budget rate.", "Unit price = net / quantity, then minimum by article/year like Power BI.",
"Preis-Waterfall, Ausreisserliste, Trendlinie je Artikel, Drilldown Lieferant -> Artikel.", "Pivot nach Jahr, Artikel-Hotlist, Verlaufslinie je Artikel.",
"price waterfall, outlier list, trend line by article, drilldown supplier -> article.", "pivot by year, article hotlist, trend line by article.",
"Naechster Schritt: Preisbasis in EKPO final klaeren und historische Vergleichsperiode definieren.", "Naechster Schritt: Lieferanten- und Warengruppennamen aus Data/Data2 als Mapping anbinden.",
"Next step: finalise EKPO price basis and define historical comparison period.", "Next step: connect supplier and material group names from Data/Data2 as mapping.",
_liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO",
_liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO",
Icons.Material.Filled.TrendingUp, Icons.Material.Filled.TrendingUp,
@@ -1157,8 +1157,8 @@
new("Spend Management", "Spend management", "Spend CHF", "spend CHF", "Jahr / Lieferant / Warengruppe / Artikel", "EKKO+EKPO", _liveState.EkpoLoaded ? "Live-Probe" : "wartet auf EKPO", _liveState.EkpoLoaded ? "live sample" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning), new("Spend Management", "Spend management", "Spend CHF", "spend CHF", "Jahr / Lieferant / Warengruppe / Artikel", "EKKO+EKPO", _liveState.EkpoLoaded ? "Live-Probe" : "wartet auf EKPO", _liveState.EkpoLoaded ? "live sample" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning),
new("Spend Management", "Spend management", "Top-10-Lieferantenanteil %", "top-10 supplier share %", "Jahr / Warengruppe", "EKPO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning), new("Spend Management", "Spend management", "Top-10-Lieferantenanteil %", "top-10 supplier share %", "Jahr / Warengruppe", "EKPO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning),
new("Lieferantenrisiko", "Supplier risk", "Risiko-Score 0-100", "Risk score 0-100", "Lieferant / Warengruppe / Artikel", "EKKO+EKPO+EKET", _liveState.EkpoLoaded && _liveState.EketLoaded ? "bereit" : "wartet auf Tabellen", _liveState.EkpoLoaded && _liveState.EketLoaded ? "ready" : "waiting for tables", _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning), new("Lieferantenrisiko", "Supplier risk", "Risiko-Score 0-100", "Risk score 0-100", "Lieferant / Warengruppe / Artikel", "EKKO+EKPO+EKET", _liveState.EkpoLoaded && _liveState.EketLoaded ? "bereit" : "wartet auf Tabellen", _liveState.EkpoLoaded && _liveState.EketLoaded ? "ready" : "waiting for tables", _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning),
new("Preisabweichung", "Price variance", "Preisdelta % / CHF", "price delta % / CHF", "Artikel / Lieferant / Jahr", "EKPO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning), new("Preisentwicklung", "Price trend", "Min(Netwr CHF/Stk)", "Min(Netwr CHF/unit)", "Artikel / Jahr", "EKPO+EKKO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning),
new("Preisabweichung", "Price variance", "Letzter Preis vs. Vorjahr", "last price vs. prior year", "Artikel / Lieferant", "EKPO+EKKO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning), new("PowerBI Abgleich", "Power BI alignment", "gleiche Aggregation", "same aggregation", "Artikel / Lieferant / Jahr", "x.pbix", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning),
new("Maverick Buying", "Maverick buying", "Anteil ausserhalb Vertrag", "share outside contract", "Einkaeufer / Lieferant / Warengruppe", "EKKO+EKPO+Kontrakt", "Konzept", "concept", Color.Info), new("Maverick Buying", "Maverick buying", "Anteil ausserhalb Vertrag", "share outside contract", "Einkaeufer / Lieferant / Warengruppe", "EKKO+EKPO+Kontrakt", "Konzept", "concept", Color.Info),
new("Rahmenvertragsnutzung", "Contract utilisation", "Abrufquote %", "consumption rate %", "Kontrakt / Lieferant / Artikel", "EKPO+EKET", _liveState.EketLoaded ? "teilweise" : "wartet auf EKET", _liveState.EketLoaded ? "partial" : "waiting for EKET", _liveState.EketLoaded ? Color.Success : Color.Warning), new("Rahmenvertragsnutzung", "Contract utilisation", "Abrufquote %", "consumption rate %", "Kontrakt / Lieferant / Artikel", "EKPO+EKET", _liveState.EketLoaded ? "teilweise" : "wartet auf EKET", _liveState.EketLoaded ? "partial" : "waiting for EKET", _liveState.EketLoaded ? Color.Success : Color.Warning),
new("Liefertermin-Risiko", "Delivery due-date risk", "Ueberfaelliger offener Wert CHF", "overdue open value CHF", "Monat / Lieferant / Artikel", "EKET+EKPO", _liveState.EketLoaded ? "bereit" : "wartet auf EKET", _liveState.EketLoaded ? "ready" : "waiting for EKET", _liveState.EketLoaded ? Color.Success : Color.Warning), new("Liefertermin-Risiko", "Delivery due-date risk", "Ueberfaelliger offener Wert CHF", "overdue open value CHF", "Monat / Lieferant / Artikel", "EKET+EKPO", _liveState.EketLoaded ? "bereit" : "wartet auf EKET", _liveState.EketLoaded ? "ready" : "waiting for EKET", _liveState.EketLoaded ? Color.Success : Color.Warning),
@@ -1177,7 +1177,7 @@
new("openQuantity", "Offene Menge", "Open quantity", "Qty"), new("openQuantity", "Offene Menge", "Open quantity", "Qty"),
new("contractValue", "Kontrakt-Restwert", "Contract remaining value", "CHF"), new("contractValue", "Kontrakt-Restwert", "Contract remaining value", "CHF"),
new("deliveryRisk", "Liefertermin-Risiko", "Delivery due-date risk", "CHF"), new("deliveryRisk", "Liefertermin-Risiko", "Delivery due-date risk", "CHF"),
new("priceVariance", "Preisabweichung", "Price variance", "CHF"), new("priceVariance", "Preisentwicklung CHF", "Price trend CHF", "CHF"),
new("spendConcentration", "Spend-Konzentration", "Spend concentration", "CHF"), new("spendConcentration", "Spend-Konzentration", "Spend concentration", "CHF"),
new("dataQuality", "Datenqualitaet", "Data quality", "Issues"), new("dataQuality", "Datenqualitaet", "Data quality", "Issues"),
new("supplierScore", "Lieferantenperformance", "Supplier performance", "%") new("supplierScore", "Lieferantenperformance", "Supplier performance", "%")
@@ -1246,10 +1246,10 @@
], ],
"ideen/preisabweichung" => "ideen/preisabweichung" =>
[ [
new("Preissteigerungen", "Price increases", _liveState.PriceVarianceRows.Count.ToString("N0"), "2026 gegen 2025", "2026 vs 2025"), new("Artikelpreise", "Article prices", _liveState.PriceVarianceRows.Count.ToString("N0"), "PowerBI Pivot", "Power BI pivot"),
new("Top Wirkung", "Top impact", _liveState.PriceVarianceChartRows.Count > 0 ? FormatChf(_liveState.PriceVarianceChartRows.Max(x => x.Value)) : "-", "CHF Effekt", "CHF effect"), new("Min Stueckpreis", "Min unit price", _liveState.PriceVarianceChartRows.Count > 0 ? FormatChf(_liveState.PriceVarianceChartRows.Min(x => x.Value)) : "-", "Netwr/Menge", "Netwr/quantity"),
new("Lieferanten", "Suppliers", _liveState.PriceVarianceChartRows.Count.ToString("N0"), "mit Abweichung", "with variance"), new("Jahre", "Years", _liveState.PriceVarianceChartRows.Count.ToString("N0"), "EKKO.Bedat", "EKKO.Bedat"),
new("Datenbasis", "Data basis", _liveState.EkpoLoaded ? "EKPO + EKKO" : "-", "Stueckpreis", "unit price") new("Datenbasis", "Data basis", _liveState.EkpoLoaded ? "EKPO + EKKO" : "-", "Min(Netwr CHF/Stk)", "Min(Netwr CHF/unit)")
], ],
"ideen/spend-konzentration" => "ideen/spend-konzentration" =>
[ [
@@ -1287,10 +1287,10 @@
], ],
"ideen/preisabweichung" => "ideen/preisabweichung" =>
[ [
new("Vergleich", "Comparison", "2026 vs 2025", "EKKO.Bedat", "SAP live"), new("PowerBI Kennzahl", "Power BI measure", "Min(Netwr CHF/Stk)", "EKPO", "SAP live"),
new("Preis", "Price", "NETWR / MENGE", "EKPO", "SAP live"), new("Preis", "Price", "NETWR / MENGE", "EKPO", "SAP live"),
new("CHF Wirkung", "CHF impact", FormatChf(_liveState.PriceVarianceChartRows.Sum(x => x.Value)), "Preisdelta * Menge", "SAP live"), new("Jahresspalten", "Year columns", $"{_liveState.PriceVarianceChartRows.Count:N0}", "EKKO.Bedat Jahr", "SAP live"),
new("Hotlist", "Hotlist", $"{_liveState.PriceVarianceRows.Count:N0}", "Lieferant / Artikel", "SAP live") new("Hotlist", "Hotlist", $"{_liveState.PriceVarianceRows.Count:N0}", "Artikel / Jahr", "SAP live")
], ],
"ideen/spend-konzentration" => "ideen/spend-konzentration" =>
[ [
@@ -1312,7 +1312,7 @@
private string SelectedIdeaAnalysisTitleDe => CurrentPurchasingPage switch private string SelectedIdeaAnalysisTitleDe => CurrentPurchasingPage switch
{ {
"ideen/liefertermin-risiko" => "Liefertermin-Risiko produktiv", "ideen/liefertermin-risiko" => "Liefertermin-Risiko produktiv",
"ideen/preisabweichung" => "Preisabweichung produktiv", "ideen/preisabweichung" => "Preisentwicklung CHF produktiv",
"ideen/spend-konzentration" => "Spend-Konzentration produktiv", "ideen/spend-konzentration" => "Spend-Konzentration produktiv",
"ideen/datenqualitaet" => "Datenqualitaet produktiv", "ideen/datenqualitaet" => "Datenqualitaet produktiv",
_ => "Einkauf Analyse" _ => "Einkauf Analyse"
@@ -1321,7 +1321,7 @@
private string SelectedIdeaAnalysisTitleEn => CurrentPurchasingPage switch private string SelectedIdeaAnalysisTitleEn => CurrentPurchasingPage switch
{ {
"ideen/liefertermin-risiko" => "Delivery due-date risk productive", "ideen/liefertermin-risiko" => "Delivery due-date risk productive",
"ideen/preisabweichung" => "Price variance productive", "ideen/preisabweichung" => "Price trend CHF productive",
"ideen/spend-konzentration" => "Spend concentration productive", "ideen/spend-konzentration" => "Spend concentration productive",
"ideen/datenqualitaet" => "Data quality productive", "ideen/datenqualitaet" => "Data quality productive",
_ => "Purchasing analysis" _ => "Purchasing analysis"
@@ -1330,7 +1330,7 @@
private string SelectedIdeaAnalysisDescriptionDe => CurrentPurchasingPage switch private string SelectedIdeaAnalysisDescriptionDe => CurrentPurchasingPage switch
{ {
"ideen/liefertermin-risiko" => "Offene EKET-Mengen werden bewertet und nach Faelligkeit, Lieferant und Artikel priorisiert.", "ideen/liefertermin-risiko" => "Offene EKET-Mengen werden bewertet und nach Faelligkeit, Lieferant und Artikel priorisiert.",
"ideen/preisabweichung" => "Netto-Stueckpreise werden 2026 gegen 2025 verglichen und nach CHF-Wirkung sortiert.", "ideen/preisabweichung" => "Netto-Stueckpreise werden wie in PowerBI als Minimum je Artikel und Jahr gezeigt.",
"ideen/spend-konzentration" => "Lieferantenspend wird als Pareto ausgewertet, um Abhaengigkeit und Buendelungspotenzial zu zeigen.", "ideen/spend-konzentration" => "Lieferantenspend wird als Pareto ausgewertet, um Abhaengigkeit und Buendelungspotenzial zu zeigen.",
"ideen/datenqualitaet" => "Pflichtfelder und Nullwerte im Einkauf-Cache werden als Qualitaetsampel gezaehlt.", "ideen/datenqualitaet" => "Pflichtfelder und Nullwerte im Einkauf-Cache werden als Qualitaetsampel gezaehlt.",
_ => "Echte Analyse aus dem Einkauf-Cache." _ => "Echte Analyse aus dem Einkauf-Cache."
@@ -1339,7 +1339,7 @@
private string SelectedIdeaAnalysisDescriptionEn => CurrentPurchasingPage switch private string SelectedIdeaAnalysisDescriptionEn => CurrentPurchasingPage switch
{ {
"ideen/liefertermin-risiko" => "Open EKET quantities are valued and prioritised by due date, supplier and article.", "ideen/liefertermin-risiko" => "Open EKET quantities are valued and prioritised by due date, supplier and article.",
"ideen/preisabweichung" => "Net unit prices are compared 2026 against 2025 and sorted by CHF impact.", "ideen/preisabweichung" => "Net unit prices are shown like in Power BI as minimum by article and year.",
"ideen/spend-konzentration" => "Supplier spend is evaluated as pareto to show dependency and bundling potential.", "ideen/spend-konzentration" => "Supplier spend is evaluated as pareto to show dependency and bundling potential.",
"ideen/datenqualitaet" => "Required fields and zero values in the purchasing cache are counted as quality indicators.", "ideen/datenqualitaet" => "Required fields and zero values in the purchasing cache are counted as quality indicators.",
_ => "Real analysis from the purchasing cache." _ => "Real analysis from the purchasing cache."
@@ -1348,7 +1348,7 @@
private string SelectedIdeaChartTitleDe => CurrentPurchasingPage switch private string SelectedIdeaChartTitleDe => CurrentPurchasingPage switch
{ {
"ideen/liefertermin-risiko" => "Offener Wert nach Faelligkeit", "ideen/liefertermin-risiko" => "Offener Wert nach Faelligkeit",
"ideen/preisabweichung" => "Preissteigerungswirkung nach Lieferant", "ideen/preisabweichung" => "Min. Stueckpreis nach Jahr",
"ideen/spend-konzentration" => "Top Lieferanten Spend", "ideen/spend-konzentration" => "Top Lieferanten Spend",
"ideen/datenqualitaet" => "Datenqualitaetsfehler", "ideen/datenqualitaet" => "Datenqualitaetsfehler",
_ => "Analyse" _ => "Analyse"
@@ -1357,7 +1357,7 @@
private string SelectedIdeaChartTitleEn => CurrentPurchasingPage switch private string SelectedIdeaChartTitleEn => CurrentPurchasingPage switch
{ {
"ideen/liefertermin-risiko" => "Open value by due date", "ideen/liefertermin-risiko" => "Open value by due date",
"ideen/preisabweichung" => "Price increase impact by supplier", "ideen/preisabweichung" => "Min. unit price by year",
"ideen/spend-konzentration" => "Top supplier spend", "ideen/spend-konzentration" => "Top supplier spend",
"ideen/datenqualitaet" => "Data quality issues", "ideen/datenqualitaet" => "Data quality issues",
_ => "Analysis" _ => "Analysis"
@@ -197,7 +197,7 @@ public class DatabaseSeedService : IDatabaseSeedService
Link("purchasing-ideas-overview", "purchasing-ideas", "Uebersicht", "Overview", "Lightbulb", "einkauf/ideen", 10, "All"), Link("purchasing-ideas-overview", "purchasing-ideas", "Uebersicht", "Overview", "Lightbulb", "einkauf/ideen", 10, "All"),
Link("purchasing-idea-data-service", "purchasing-ideas", "Einkauf-Datenservice", "Purchasing data service", "Storage", "einkauf/ideen/datenservice", 20, "All"), Link("purchasing-idea-data-service", "purchasing-ideas", "Einkauf-Datenservice", "Purchasing data service", "Storage", "einkauf/ideen/datenservice", 20, "All"),
Link("purchasing-idea-delivery-risk", "purchasing-ideas", "Liefertermin-Risiko", "Delivery due-date risk", "PendingActions", "einkauf/ideen/liefertermin-risiko", 30, "All"), Link("purchasing-idea-delivery-risk", "purchasing-ideas", "Liefertermin-Risiko", "Delivery due-date risk", "PendingActions", "einkauf/ideen/liefertermin-risiko", 30, "All"),
Link("purchasing-idea-price-variance", "purchasing-ideas", "Preisabweichung", "Price variance", "TrendingUp", "einkauf/ideen/preisabweichung", 40, "All"), Link("purchasing-idea-price-variance", "purchasing-ideas", "Preisentwicklung", "Price trend", "TrendingUp", "einkauf/ideen/preisabweichung", 40, "All"),
Link("purchasing-idea-spend-concentration", "purchasing-ideas", "Spend-Konzentration", "Spend concentration", "PieChart", "einkauf/ideen/spend-konzentration", 50, "All"), Link("purchasing-idea-spend-concentration", "purchasing-ideas", "Spend-Konzentration", "Spend concentration", "PieChart", "einkauf/ideen/spend-konzentration", 50, "All"),
Link("purchasing-idea-data-quality", "purchasing-ideas", "Datenqualitaet", "Data quality", "FactCheck", "einkauf/ideen/datenqualitaet", 60, "All"), Link("purchasing-idea-data-quality", "purchasing-ideas", "Datenqualitaet", "Data quality", "FactCheck", "einkauf/ideen/datenqualitaet", 60, "All"),
Link("purchasing-kpi-catalog", "purchasing", "Kennzahlen-Katalog", "KPI catalogue", "Checklist", "einkauf/kennzahlen", 70, "All"), Link("purchasing-kpi-catalog", "purchasing", "Kennzahlen-Katalog", "KPI catalogue", "Checklist", "einkauf/kennzahlen", 70, "All"),
@@ -40,6 +40,7 @@ public sealed class PurchasingDashboardLiveState
public List<PurchasingLiveChartPoint> PriceVarianceChartRows { get; set; } = []; public List<PurchasingLiveChartPoint> PriceVarianceChartRows { get; set; } = [];
public List<PurchasingLiveChartPoint> SpendConcentrationChartRows { get; set; } = []; public List<PurchasingLiveChartPoint> SpendConcentrationChartRows { get; set; } = [];
public List<PurchasingLiveChartPoint> DataQualityChartRows { get; set; } = []; public List<PurchasingLiveChartPoint> DataQualityChartRows { get; set; } = [];
public List<PurchasingLiveChartPoint> PriceTrendChartRows { get; set; } = [];
public List<PurchasingIdeaAnalysisRow> DeliveryRiskRows { get; set; } = []; public List<PurchasingIdeaAnalysisRow> DeliveryRiskRows { get; set; } = [];
public List<PurchasingIdeaAnalysisRow> PriceVarianceRows { get; set; } = []; public List<PurchasingIdeaAnalysisRow> PriceVarianceRows { get; set; } = [];
public List<PurchasingIdeaAnalysisRow> SpendConcentrationRows { get; set; } = []; public List<PurchasingIdeaAnalysisRow> SpendConcentrationRows { get; set; } = [];
@@ -256,59 +256,40 @@ WITH priced AS (
COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Supplier, COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Supplier,
COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') AS Article, COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') AS Article,
substr(k.Bedat, 1, 4) AS Year, substr(k.Bedat, 1, 4) AS Year,
SUM(CAST(p.Netwr AS REAL)) AS Value, MIN(CASE WHEN CAST(p.Menge AS REAL) = 0 THEN NULL ELSE CAST(p.Netwr AS REAL) / CAST(p.Menge AS REAL) END) AS UnitPrice
SUM(CAST(p.Menge AS REAL)) AS Quantity
FROM PurchasingEkpoCache p FROM PurchasingEkpoCache p
LEFT JOIN PurchasingEkkoCache k ON k.Ebeln = p.Ebeln LEFT JOIN PurchasingEkkoCache k ON k.Ebeln = p.Ebeln
WHERE COALESCE(p.Loekz, '') = '' AND CAST(p.Menge AS REAL) > 0 AND k.Bedat IS NOT NULL AND k.Bedat <> '' AND " + joinedEkkoPeriod + @" WHERE COALESCE(p.Loekz, '') = '' AND CAST(p.Menge AS REAL) > 0 AND k.Bedat IS NOT NULL AND k.Bedat <> '' AND " + joinedEkkoPeriod + @"
GROUP BY Supplier, Article, Year GROUP BY Supplier, Article, Year
),
current_year AS (
SELECT Supplier, Article, Value, Quantity, Value / Quantity AS UnitPrice
FROM priced
WHERE Year = '2026'
),
previous_year AS (
SELECT Supplier, Article, Value / Quantity AS UnitPrice
FROM priced
WHERE Year = '2025' AND Quantity > 0
) )
SELECT SELECT
c.Supplier || ' / ' || c.Article AS Label, Supplier || ' / ' || Article AS Label,
printf('%.1f%%', ((c.UnitPrice - p.UnitPrice) / p.UnitPrice) * 100.0) AS Value, 'CHF ' || printf('%.2f', UnitPrice) AS Value,
'Wirkung CHF ' || printf('%,.0f', (c.UnitPrice - p.UnitPrice) * c.Quantity) AS Detail, 'Jahr ' || Year || ' | PowerBI: Min(Netwr CHF/Stk)' AS Detail,
CASE WHEN ((c.UnitPrice - p.UnitPrice) / p.UnitPrice) >= 0.10 THEN 'High' CASE WHEN UnitPrice > 1000 THEN 'High'
WHEN ((c.UnitPrice - p.UnitPrice) / p.UnitPrice) >= 0.03 THEN 'Medium' WHEN UnitPrice > 100 THEN 'Medium'
ELSE 'Low' END AS Severity ELSE 'Low' END AS Severity
FROM current_year c FROM priced
JOIN previous_year p ON p.Supplier = c.Supplier AND p.Article = c.Article WHERE UnitPrice IS NOT NULL
WHERE p.UnitPrice > 0 AND c.UnitPrice > p.UnitPrice ORDER BY Year DESC, UnitPrice DESC
ORDER BY (c.UnitPrice - p.UnitPrice) * c.Quantity DESC
LIMIT 10;", cancellationToken); LIMIT 10;", cancellationToken);
state.PriceVarianceChartRows = await ExecuteChartRowsAsync(conn, @" state.PriceVarianceChartRows = await ExecuteChartRowsAsync(conn, @"
WITH priced AS ( WITH priced AS (
SELECT SELECT
COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Supplier,
COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') AS Article,
substr(k.Bedat, 1, 4) AS Year, substr(k.Bedat, 1, 4) AS Year,
SUM(CAST(p.Netwr AS REAL)) AS Value, COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') AS Article,
SUM(CAST(p.Menge AS REAL)) AS Quantity MIN(CASE WHEN CAST(p.Menge AS REAL) = 0 THEN NULL ELSE CAST(p.Netwr AS REAL) / CAST(p.Menge AS REAL) END) AS UnitPrice
FROM PurchasingEkpoCache p FROM PurchasingEkpoCache p
LEFT JOIN PurchasingEkkoCache k ON k.Ebeln = p.Ebeln LEFT JOIN PurchasingEkkoCache k ON k.Ebeln = p.Ebeln
WHERE COALESCE(p.Loekz, '') = '' AND CAST(p.Menge AS REAL) > 0 AND k.Bedat IS NOT NULL AND k.Bedat <> '' AND " + joinedEkkoPeriod + @" WHERE COALESCE(p.Loekz, '') = '' AND CAST(p.Menge AS REAL) > 0 AND k.Bedat IS NOT NULL AND k.Bedat <> '' AND " + joinedEkkoPeriod + @"
GROUP BY Supplier, Article, Year GROUP BY Year, Article
),
delta AS (
SELECT c.Supplier, (c.Value / c.Quantity - p.Value / p.Quantity) * c.Quantity AS Impact
FROM priced c
JOIN priced p ON p.Supplier = c.Supplier AND p.Article = c.Article
WHERE c.Year = '2026' AND p.Year = '2025' AND p.Quantity > 0 AND c.Quantity > 0 AND c.Value / c.Quantity > p.Value / p.Quantity
) )
SELECT Supplier, SUM(Impact) AS Value SELECT Year, MIN(UnitPrice) AS Value
FROM delta FROM priced
GROUP BY Supplier WHERE UnitPrice IS NOT NULL
ORDER BY Value DESC GROUP BY Year
LIMIT 6;", cancellationToken); ORDER BY Year;", cancellationToken);
state.PriceTrendChartRows = state.PriceVarianceChartRows.ToList();
state.SpendConcentrationChartRows = await ExecuteChartRowsAsync(conn, @" state.SpendConcentrationChartRows = await ExecuteChartRowsAsync(conn, @"
SELECT 'Lieferant ' || COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value SELECT 'Lieferant ' || COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value
@@ -147,21 +147,47 @@ Technische Logik:
Die Seite `/einkauf` zeigt nun echte Werte aus dem SAP-Cache: Die Seite `/einkauf` zeigt nun echte Werte aus dem SAP-Cache:
- `Spend total`: Summe `EKPOSet.Netwr` aus dem Cache. - `Spend total`: Summe `EKPOSet.Netwr` aus dem Cache, begrenzt auf den gewaehlten Zeitraum.
- `Offene Bestellungen`: Anzahl EKKO-Belege seit Jahresbeginn. - `Offene Bestellungen`: Anzahl EKKO-Belege im gewaehlten Zeitraum.
- `Kontrakte`: offener Restwert aus `EKET.Menge - EKET.Wemng` bewertet mit EKPO-Netto-Stueckwert. - `Kontrakte`: offener Restwert aus `EKET.Menge - EKET.Wemng` bewertet mit EKPO-Netto-Stueckwert.
- `Offener Bestellwert`: berechnet aus EKET-Offenmenge und EKPO-Netto-Stueckwert. - `Offener Bestellwert`: berechnet aus EKET-Offenmenge und EKPO-Netto-Stueckwert.
- `Offene Menge`: Summe offener EKET-Mengen. - `Offene Menge`: Summe offener EKET-Mengen.
- Top-Lieferant, Top-Warengruppe und Top-Artikel werden aus EKPO gruppiert. - Top-Lieferant, Top-Warengruppe und Top-Artikel werden aus EKPO gruppiert.
- Spend-, Offenwert- und Kontrakt-Diagramme verwenden Cache-Gruppierungen, sofern der Cache gefuellt ist. - Spend-, Offenwert- und Kontrakt-Diagramme verwenden Cache-Gruppierungen, sofern der Cache gefuellt ist.
- Ist der Cache leer oder nicht erreichbar, faellt das Dashboard auf eine begrenzte SAP-Live-Probe zurueck. - Ist der Cache leer oder nicht erreichbar, faellt das Dashboard auf eine begrenzte SAP-Live-Probe zurueck.
- Der Standardzeitraum ist rollierend auf die letzten drei Kalenderjahre bis heute gesetzt. Die Datumsabgrenzung erfolgt im Dashboard ueber `Von Monat` und `Bis Monat`.
## PowerBI-Abgleich
Das Einkaufsdashboard wurde gegen die sichtbaren Auswertungen aus `x.pbix` abgeglichen:
- `Besch.Volumen CHF/Lieferant`: `Sum(EKPOSet.Netwr CHF)` nach Jahr, Lieferant, Warengruppe und Artikel.
- `Eink.Vol. CHF / Lieferant Kuchen`: `Sum(EKPOSet.Netwr CHF)` nach Lieferant.
- `Balken Vol./Lief/WG`: `Sum(EKPOSet.Netwr CHF)` nach Jahr und Lieferant.
- `Diagramm Vol./WG`: `Sum(EKPOSet.Netwr CHF)` nach Jahr und Warengruppe.
- `Eink.Vol. CHF / Region`: `Sum(EKPOSet.Netwr CHF)` nach Region.
- `Preisentwicklung CHF`: `Min(EKPOSet.Netwr CHF/Stk)` nach Artikel und Jahr.
- `Matrix Vol./WG`: `Sum(EKPOSet.Netwr CHF)` nach Warengruppe, Lieferant und Artikel.
Umgesetzt ist die gleiche Kernaggregation:
- Spend und Volumen verwenden `SUM(EKPO.Netwr)` mit Zeitraumfilter auf `EKKO.Bedat`.
- Preisentwicklung verwendet `MIN(EKPO.Netwr / EKPO.Menge)` je Artikel und Jahr mit Zeitraumfilter auf `EKKO.Bedat`.
- Offene Werte verwenden `MAX(EKET.Menge - EKET.Wemng, 0) * (EKPO.Netwr / EKPO.Menge)`.
Noch nicht final 1:1 ist die Namensauflösung:
- PowerBI nutzt fuer Lieferanten- und Warengruppennamen `Data.Name`, `Data.Lieferant`, `Data (2).Warengruppe` und `Data (2).WG komplett`.
- Der aktuelle SAP-OData-Service liefert produktiv `EKKOSet`, `EKPOSet` und `eketSet`.
- Tests auf `Data`, `Data2`, `DataSet` und `Data2Set` liefern aktuell `404 Resource not found`.
- Bis diese Mapping-Quelle angebunden ist, zeigt das Dashboard Lieferantennummern und Warengruppen-Codes statt vollstaendiger Namen.
## Ideen und Kennzahlen-Katalog ## Ideen und Kennzahlen-Katalog
Der Ideenbereich wurde fuer den Einkauf erweitert: Der Ideenbereich wurde fuer den Einkauf erweitert:
- Lieferantenrisiko. - Lieferantenrisiko.
- Preisabweichung. - Preisentwicklung CHF.
- Maverick Buying. - Maverick Buying.
- Rahmenvertragsnutzung. - Rahmenvertragsnutzung.
- Working Capital. - Working Capital.
@@ -178,8 +204,8 @@ Der separate Kennzahlen-Katalog enthaelt nun konkrete Ausbau-KPIs mit Dimension
- Spend CHF. - Spend CHF.
- Top-10-Lieferantenanteil. - Top-10-Lieferantenanteil.
- Risiko-Score 0-100. - Risiko-Score 0-100.
- Preisdelta in Prozent und CHF. - Min. Netto-Stueckpreis nach Artikel und Jahr.
- Letzter Preis vs. Vorjahr. - Preisentwicklung analog PowerBI.
- Anteil ausserhalb Vertrag. - Anteil ausserhalb Vertrag.
- Abrufquote. - Abrufquote.
- Ueberfaelliger offener Wert. - Ueberfaelliger offener Wert.
@@ -206,8 +232,8 @@ Die Simulation nutzt feste Canvas-Groessen, sichtbare Achsen, waehlbare Diagramm
Die technische Vollbasis ist geladen. Fuer fachlich finale Management-Sichten muessen noch diese Abgrenzungen abgestimmt werden: Die technische Vollbasis ist geladen. Fuer fachlich finale Management-Sichten muessen noch diese Abgrenzungen abgestimmt werden:
- Jahres-/Periodenfilter fuer `EKKOSet.Bedat`. - Mapping-Quelle fuer Lieferantennamen, Region und Warengruppentexte bereitstellen oder als eigene Cache-Tabelle laden.
- Periodenlogik fuer historische und offene Werte. - PowerBI-Zielwerte mit Marco/Finanzen anhand eines konkreten Monats und Lieferanten gegenpruefen.
- Kontrakte und offene Verpflichtungen, inkl. fachlicher Abgrenzung von normalen Bestellungen und Umlagerungen. - Kontrakte und offene Verpflichtungen, inkl. fachlicher Abgrenzung von normalen Bestellungen und Umlagerungen.
- Lieferantenbewertung / Performance, falls im SAP-System als OData- oder HANA-Quelle verfuegbar. - Lieferantenbewertung / Performance, falls im SAP-System als OData- oder HANA-Quelle verfuegbar.