From a41ef0a564bbd62e04b4705143d7943d1481ee0a Mon Sep 17 00:00:00 2001 From: metacube Date: Fri, 5 Jun 2026 14:04:27 +0200 Subject: [PATCH] Align purchasing analytics with Power BI --- .../Pages/PurchasingDashboard.razor | 94 +++++++++---------- .../Services/DatabaseSeedService.cs | 2 +- .../Services/IPurchasingDashboardService.cs | 1 + .../Services/PurchasingDashboardService.cs | 55 ++++------- .../docs/PURCHASING_DASHBOARD_2026-06-05.md | 40 ++++++-- 5 files changed, 100 insertions(+), 92 deletions(-) diff --git a/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor b/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor index 4b9268d..5e8b899 100644 --- a/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor +++ b/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor @@ -800,7 +800,7 @@ private IReadOnlyList 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("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("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), @@ -814,7 +814,7 @@ private IReadOnlyList 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("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("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) @@ -876,27 +876,27 @@ ]), new( "ideen/preisabweichung", - "Preisabweichung", - "Price variance", - "Preisveraenderungen pro Artikel/Lieferant.", - "Price changes by article/supplier.", - "Preissteigerungen und Ausreisser werden gegen Vorjahr, letzte Bestellung oder Budget-/Referenzpreis sichtbar gemacht.", - "Price increases and outliers are shown against prior year, last order or budget/reference price.", + "Preisentwicklung CHF", + "Price trend CHF", + "PowerBI-Logik pro Artikel/Jahr.", + "Power BI logic by article/year.", + "Minimum Netto-Stueckpreis wird pro Artikel und Jahr sichtbar gemacht.", + "Minimum net unit price is shown by article and year.", "EKPO.Netwr, EKPO.Menge, EKPO.Matnr, EKKO.Bedat, EKKO.Lifnr, FX/Budgetkurse", - "Preisdelta %, Preisdelta CHF, letzter Preis, Referenzpreis, potentieller Mehrpreis.", - "price delta %, price delta CHF, last price, reference price, potential extra cost.", - "Netto-Stueckpreis je Artikel/Lieferant/Periode bilden und gegen Referenzperiode vergleichen; Mengenwirkung separat ausweisen.", - "calculate net unit price by article/supplier/period and compare against reference period; show quantity effect separately.", - "Naechster Schritt: Referenzlogik festlegen: Vorjahr, letzter Preis oder Budgetpreis.", - "Next step: define reference logic: prior year, last price or budget price.", + "Min(Netwr CHF/Stk), Artikel, Jahr, Lieferantenslicer.", + "Min(Netwr CHF/unit), article, year, supplier slicer.", + "Netto-Stueckpreis als Netwr/Menge bilden und Minimum je Artikel/Jahr ausweisen.", + "calculate net unit price as Netwr/quantity and show minimum by article/year.", + "Naechster Schritt: Lieferantennamen aus Data-Quelle mitcachen.", + "Next step: cache supplier names from Data source.", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", Icons.Material.Filled.TrendingUp, _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("Referenzperiode", "Reference period", "Vergleich gegen Vorjahr oder letzten Einkaufspreis waehlen.", "choose comparison against prior year or last purchase price."), - new("Ausreisser", "Outliers", "Top Preissteigerungen nach CHF-Wirkung und Prozent anzeigen.", "show top price increases by CHF impact and percent.") + new("Jahr", "Year", "PowerBI nutzt EKKO.Bedat Jahr als Spalte.", "Power BI uses EKKO.Bedat year as column."), + new("Minimum", "Minimum", "PowerBI aggregiert mit Min(Netwr CHF/Stk).", "Power BI aggregates with Min(Netwr CHF/unit).") ]), new( "ideen/spend-konzentration", @@ -971,21 +971,21 @@ Icons.Material.Filled.WarningAmber, _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning), new( - "Preisabweichung", - "Price variance", - "Preisveraenderungen pro Artikel und Lieferant.", - "Price changes by article and supplier.", - "Preissteigerungen, Ausreisser und Verhandlungspotenzial transparent machen.", - "Make price increases, outliers and negotiation potential transparent.", - "EKPO, EKKO.Bedat, Waehrung/FX, Artikel, Lieferant", - "Preisdelta CHF, Preisdelta %, letzter Preis, Vorjahrespreis, Budgetpreis, Einsparpotenzial.", - "price delta CHF, price delta %, last price, prior-year price, budget price, savings potential.", - "Netto-Stueckpreis je Artikel/Lieferant/Periode bilden und gegen Referenzperiode oder Budgetkurs vergleichen.", - "Calculate net unit price by article/supplier/period and compare against reference period or budget rate.", - "Preis-Waterfall, Ausreisserliste, Trendlinie je Artikel, Drilldown Lieferant -> Artikel.", - "price waterfall, outlier list, trend line by article, drilldown supplier -> article.", - "Naechster Schritt: Preisbasis in EKPO final klaeren und historische Vergleichsperiode definieren.", - "Next step: finalise EKPO price basis and define historical comparison period.", + "Preisentwicklung CHF", + "Price trend CHF", + "PowerBI-Preislogik nach Artikel und Jahr.", + "Power BI price logic by article and year.", + "Minimum Netto-Stueckpreis wie in PowerBI transparent machen.", + "Make minimum net unit price transparent like in Power BI.", + "EKPO.Netwr, EKPO.Menge, EKPO.Matnr, EKPO.Txz01, EKKO.Bedat", + "Min(Netwr CHF/Stk), Artikel, Jahr, Lieferantenslicer.", + "Min(Netwr CHF/unit), article, year, supplier slicer.", + "Stueckpreis = Netto / Menge, danach Minimum je Artikel/Jahr wie PowerBI.", + "Unit price = net / quantity, then minimum by article/year like Power BI.", + "Pivot nach Jahr, Artikel-Hotlist, Verlaufslinie je Artikel.", + "pivot by year, article hotlist, trend line by article.", + "Naechster Schritt: Lieferanten- und Warengruppennamen aus Data/Data2 als Mapping anbinden.", + "Next step: connect supplier and material group names from Data/Data2 as mapping.", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", 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", "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("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("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("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("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("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), @@ -1177,7 +1177,7 @@ new("openQuantity", "Offene Menge", "Open quantity", "Qty"), new("contractValue", "Kontrakt-Restwert", "Contract remaining value", "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("dataQuality", "Datenqualitaet", "Data quality", "Issues"), new("supplierScore", "Lieferantenperformance", "Supplier performance", "%") @@ -1246,10 +1246,10 @@ ], "ideen/preisabweichung" => [ - new("Preissteigerungen", "Price increases", _liveState.PriceVarianceRows.Count.ToString("N0"), "2026 gegen 2025", "2026 vs 2025"), - new("Top Wirkung", "Top impact", _liveState.PriceVarianceChartRows.Count > 0 ? FormatChf(_liveState.PriceVarianceChartRows.Max(x => x.Value)) : "-", "CHF Effekt", "CHF effect"), - new("Lieferanten", "Suppliers", _liveState.PriceVarianceChartRows.Count.ToString("N0"), "mit Abweichung", "with variance"), - new("Datenbasis", "Data basis", _liveState.EkpoLoaded ? "EKPO + EKKO" : "-", "Stueckpreis", "unit price") + new("Artikelpreise", "Article prices", _liveState.PriceVarianceRows.Count.ToString("N0"), "PowerBI Pivot", "Power BI pivot"), + new("Min Stueckpreis", "Min unit price", _liveState.PriceVarianceChartRows.Count > 0 ? FormatChf(_liveState.PriceVarianceChartRows.Min(x => x.Value)) : "-", "Netwr/Menge", "Netwr/quantity"), + new("Jahre", "Years", _liveState.PriceVarianceChartRows.Count.ToString("N0"), "EKKO.Bedat", "EKKO.Bedat"), + new("Datenbasis", "Data basis", _liveState.EkpoLoaded ? "EKPO + EKKO" : "-", "Min(Netwr CHF/Stk)", "Min(Netwr CHF/unit)") ], "ideen/spend-konzentration" => [ @@ -1287,10 +1287,10 @@ ], "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("CHF Wirkung", "CHF impact", FormatChf(_liveState.PriceVarianceChartRows.Sum(x => x.Value)), "Preisdelta * Menge", "SAP live"), - new("Hotlist", "Hotlist", $"{_liveState.PriceVarianceRows.Count:N0}", "Lieferant / Artikel", "SAP live") + new("Jahresspalten", "Year columns", $"{_liveState.PriceVarianceChartRows.Count:N0}", "EKKO.Bedat Jahr", "SAP live"), + new("Hotlist", "Hotlist", $"{_liveState.PriceVarianceRows.Count:N0}", "Artikel / Jahr", "SAP live") ], "ideen/spend-konzentration" => [ @@ -1312,7 +1312,7 @@ private string SelectedIdeaAnalysisTitleDe => CurrentPurchasingPage switch { "ideen/liefertermin-risiko" => "Liefertermin-Risiko produktiv", - "ideen/preisabweichung" => "Preisabweichung produktiv", + "ideen/preisabweichung" => "Preisentwicklung CHF produktiv", "ideen/spend-konzentration" => "Spend-Konzentration produktiv", "ideen/datenqualitaet" => "Datenqualitaet produktiv", _ => "Einkauf Analyse" @@ -1321,7 +1321,7 @@ private string SelectedIdeaAnalysisTitleEn => CurrentPurchasingPage switch { "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/datenqualitaet" => "Data quality productive", _ => "Purchasing analysis" @@ -1330,7 +1330,7 @@ private string SelectedIdeaAnalysisDescriptionDe => CurrentPurchasingPage switch { "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/datenqualitaet" => "Pflichtfelder und Nullwerte im Einkauf-Cache werden als Qualitaetsampel gezaehlt.", _ => "Echte Analyse aus dem Einkauf-Cache." @@ -1339,7 +1339,7 @@ private string SelectedIdeaAnalysisDescriptionEn => CurrentPurchasingPage switch { "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/datenqualitaet" => "Required fields and zero values in the purchasing cache are counted as quality indicators.", _ => "Real analysis from the purchasing cache." @@ -1348,7 +1348,7 @@ private string SelectedIdeaChartTitleDe => CurrentPurchasingPage switch { "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/datenqualitaet" => "Datenqualitaetsfehler", _ => "Analyse" @@ -1357,7 +1357,7 @@ private string SelectedIdeaChartTitleEn => CurrentPurchasingPage switch { "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/datenqualitaet" => "Data quality issues", _ => "Analysis" diff --git a/TrafagSalesExporter/Services/DatabaseSeedService.cs b/TrafagSalesExporter/Services/DatabaseSeedService.cs index f5192e7..2a27a26 100644 --- a/TrafagSalesExporter/Services/DatabaseSeedService.cs +++ b/TrafagSalesExporter/Services/DatabaseSeedService.cs @@ -197,7 +197,7 @@ public class DatabaseSeedService : IDatabaseSeedService 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-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-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"), diff --git a/TrafagSalesExporter/Services/IPurchasingDashboardService.cs b/TrafagSalesExporter/Services/IPurchasingDashboardService.cs index 32f4d4e..7719c1c 100644 --- a/TrafagSalesExporter/Services/IPurchasingDashboardService.cs +++ b/TrafagSalesExporter/Services/IPurchasingDashboardService.cs @@ -40,6 +40,7 @@ public sealed class PurchasingDashboardLiveState public List PriceVarianceChartRows { get; set; } = []; public List SpendConcentrationChartRows { get; set; } = []; public List DataQualityChartRows { get; set; } = []; + public List PriceTrendChartRows { get; set; } = []; public List DeliveryRiskRows { get; set; } = []; public List PriceVarianceRows { get; set; } = []; public List SpendConcentrationRows { get; set; } = []; diff --git a/TrafagSalesExporter/Services/PurchasingDashboardService.cs b/TrafagSalesExporter/Services/PurchasingDashboardService.cs index 2b7cacd..ff33742 100644 --- a/TrafagSalesExporter/Services/PurchasingDashboardService.cs +++ b/TrafagSalesExporter/Services/PurchasingDashboardService.cs @@ -256,59 +256,40 @@ WITH priced AS ( 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, - SUM(CAST(p.Netwr AS REAL)) AS Value, - 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 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 + @" 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 - c.Supplier || ' / ' || c.Article AS Label, - printf('%.1f%%', ((c.UnitPrice - p.UnitPrice) / p.UnitPrice) * 100.0) AS Value, - 'Wirkung CHF ' || printf('%,.0f', (c.UnitPrice - p.UnitPrice) * c.Quantity) AS Detail, - CASE WHEN ((c.UnitPrice - p.UnitPrice) / p.UnitPrice) >= 0.10 THEN 'High' - WHEN ((c.UnitPrice - p.UnitPrice) / p.UnitPrice) >= 0.03 THEN 'Medium' + Supplier || ' / ' || Article AS Label, + 'CHF ' || printf('%.2f', UnitPrice) AS Value, + 'Jahr ' || Year || ' | PowerBI: Min(Netwr CHF/Stk)' AS Detail, + CASE WHEN UnitPrice > 1000 THEN 'High' + WHEN UnitPrice > 100 THEN 'Medium' ELSE 'Low' END AS Severity -FROM current_year c -JOIN previous_year p ON p.Supplier = c.Supplier AND p.Article = c.Article -WHERE p.UnitPrice > 0 AND c.UnitPrice > p.UnitPrice -ORDER BY (c.UnitPrice - p.UnitPrice) * c.Quantity DESC +FROM priced +WHERE UnitPrice IS NOT NULL +ORDER BY Year DESC, UnitPrice DESC LIMIT 10;", cancellationToken); state.PriceVarianceChartRows = await ExecuteChartRowsAsync(conn, @" WITH priced AS ( 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, - SUM(CAST(p.Netwr AS REAL)) AS Value, - SUM(CAST(p.Menge AS REAL)) AS Quantity + COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') AS Article, + 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 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 + @" - GROUP BY Supplier, Article, Year -), -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 + GROUP BY Year, Article ) -SELECT Supplier, SUM(Impact) AS Value -FROM delta -GROUP BY Supplier -ORDER BY Value DESC -LIMIT 6;", cancellationToken); +SELECT Year, MIN(UnitPrice) AS Value +FROM priced +WHERE UnitPrice IS NOT NULL +GROUP BY Year +ORDER BY Year;", cancellationToken); + state.PriceTrendChartRows = state.PriceVarianceChartRows.ToList(); state.SpendConcentrationChartRows = await ExecuteChartRowsAsync(conn, @" SELECT 'Lieferant ' || COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value diff --git a/TrafagSalesExporter/docs/PURCHASING_DASHBOARD_2026-06-05.md b/TrafagSalesExporter/docs/PURCHASING_DASHBOARD_2026-06-05.md index 12b7eb5..3e8ba35 100644 --- a/TrafagSalesExporter/docs/PURCHASING_DASHBOARD_2026-06-05.md +++ b/TrafagSalesExporter/docs/PURCHASING_DASHBOARD_2026-06-05.md @@ -147,21 +147,47 @@ Technische Logik: Die Seite `/einkauf` zeigt nun echte Werte aus dem SAP-Cache: -- `Spend total`: Summe `EKPOSet.Netwr` aus dem Cache. -- `Offene Bestellungen`: Anzahl EKKO-Belege seit Jahresbeginn. +- `Spend total`: Summe `EKPOSet.Netwr` aus dem Cache, begrenzt auf den gewaehlten Zeitraum. +- `Offene Bestellungen`: Anzahl EKKO-Belege im gewaehlten Zeitraum. - `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 Cache-Gruppierungen, sofern der Cache gefuellt ist. - 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 Der Ideenbereich wurde fuer den Einkauf erweitert: - Lieferantenrisiko. -- Preisabweichung. +- Preisentwicklung CHF. - Maverick Buying. - Rahmenvertragsnutzung. - Working Capital. @@ -178,8 +204,8 @@ Der separate Kennzahlen-Katalog enthaelt nun konkrete Ausbau-KPIs mit Dimension - Spend CHF. - Top-10-Lieferantenanteil. - Risiko-Score 0-100. -- Preisdelta in Prozent und CHF. -- Letzter Preis vs. Vorjahr. +- Min. Netto-Stueckpreis nach Artikel und Jahr. +- Preisentwicklung analog PowerBI. - Anteil ausserhalb Vertrag. - Abrufquote. - 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: -- Jahres-/Periodenfilter fuer `EKKOSet.Bedat`. -- Periodenlogik fuer historische und offene Werte. +- Mapping-Quelle fuer Lieferantennamen, Region und Warengruppentexte bereitstellen oder als eigene Cache-Tabelle laden. +- PowerBI-Zielwerte mit Marco/Finanzen anhand eines konkreten Monats und Lieferanten gegenpruefen. - Kontrakte und offene Verpflichtungen, inkl. fachlicher Abgrenzung von normalen Bestellungen und Umlagerungen. - Lieferantenbewertung / Performance, falls im SAP-System als OData- oder HANA-Quelle verfuegbar.