Add purchasing ideas tab and SAP table test

This commit is contained in:
2026-06-05 10:45:48 +02:00
parent 989ff66102
commit e7e408fc20
2 changed files with 361 additions and 0 deletions
@@ -185,6 +185,88 @@
DetailRows="@SupplierDetailRows" />
</MudTabPanel>
<MudTabPanel Text="@T("Ideen", "Ideas")" Icon="@Icons.Material.Filled.Lightbulb">
<MudGrid Spacing="2">
<MudItem xs="12" lg="8">
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
<div>
<MudText Typo="Typo.h6">@T("Weitere Einkaufsanalysen", "Additional purchasing analytics")</MudText>
<MudText Typo="Typo.body2" Class="purchasing-muted">
@T("Analysen, die dem Einkauf neben PowerBI mehr Steuerung, Risiko- und Sparpotenzial zeigen.",
"Analytics that give purchasing more steering, risk and savings potential beyond Power BI.")
</MudText>
</div>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined">
@T("Roadmap", "Roadmap")
</MudChip>
</MudStack>
<div class="purchasing-idea-grid">
@foreach (var idea in PurchasingIdeas)
{
<div class="purchasing-idea-card">
<div class="purchasing-idea-icon">
<MudIcon Icon="@idea.Icon" Color="@idea.Color" />
</div>
<div class="purchasing-idea-content">
<div class="purchasing-idea-title">
<strong>@T(idea.TitleDe, idea.TitleEn)</strong>
<MudChip T="string" Size="Size.Small" Color="@idea.Color" Variant="Variant.Outlined">@T(idea.StatusDe, idea.StatusEn)</MudChip>
</div>
<span>@T(idea.DescriptionDe, idea.DescriptionEn)</span>
<div class="purchasing-idea-meta">
<code>@idea.RequiredData</code>
<span>@T("Nutzen", "Value"): @T(idea.ValueDe, idea.ValueEn)</span>
</div>
</div>
</div>
}
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" lg="4">
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
<MudText Typo="Typo.h6" Class="mb-2">@T("Prioritaet", "Priority")</MudText>
<div class="purchasing-priority-stack">
@foreach (var priority in PurchasingIdeaPriorities)
{
<div class="purchasing-priority-row">
<MudIcon Icon="@priority.Icon" Color="@priority.Color" Size="Size.Small" />
<div>
<strong>@T(priority.TitleDe, priority.TitleEn)</strong>
<span>@T(priority.DetailDe, priority.DetailEn)</span>
</div>
</div>
}
</div>
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
<MudText Typo="Typo.h6" Class="mb-2">@T("Kennzahlen-Katalog fuer den naechsten Ausbau", "KPI catalogue for the next build-out")</MudText>
<MudTable Items="@PurchasingIdeaKpis" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>@T("Analyse", "Analysis")</MudTh>
<MudTh>@T("Kennzahl", "KPI")</MudTh>
<MudTh>@T("Dimension", "Dimension")</MudTh>
<MudTh>@T("Datenbasis", "Data basis")</MudTh>
<MudTh>@T("Status", "Status")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@T(context.AnalysisDe, context.AnalysisEn)</MudTd>
<MudTd><strong>@T(context.KpiDe, context.KpiEn)</strong></MudTd>
<MudTd>@context.Dimension</MudTd>
<MudTd><code>@context.Source</code></MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small" Color="@context.Color" Variant="Variant.Outlined">@T(context.StatusDe, context.StatusEn)</MudChip>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
</MudGrid>
</MudTabPanel>
<MudTabPanel Text="@T("PBIX Vorlage", "PBIX template")" Icon="@Icons.Material.Filled.InsertChart">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.h6" Class="mb-2">@T("Aus x.pbix uebernommene Seiten", "Pages derived from x.pbix")</MudText>
@@ -391,6 +473,34 @@
new("Matrix Vol./WG", "Pivot, Slicer", "Sum(EKPOSet.Netwr CHF)", "Warengruppe, Lieferant, Artikel")
];
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("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("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),
new("Datenqualitaet", "Data quality", "Prueft fehlende Lieferanten, Warengruppen, Artikeltexte, Waehrung, Preisbasis und Dubletten.", "Checks missing suppliers, material groups, article texts, currency, price basis and duplicates.", "EKKO, EKPO, Mapping", "Vertrauen in Kennzahlen sichern", "secure trust in KPIs", _liveState.EkkoLoaded ? "startklar" : "wartet", _liveState.EkkoLoaded ? "ready" : "waiting", Icons.Material.Filled.FactCheck, _liveState.EkkoLoaded ? Color.Success : Color.Warning)
];
private IReadOnlyList<PurchasingIdeaPriority> PurchasingIdeaPriorities =>
[
new("1. EKPO/EKET Daten reparieren", "1. Repair EKPO/EKET data", "Ohne Positionen fehlen echte Spend-, Artikel-, Warengruppen- und Termindaten.", "Without item rows, real spend, article, material-group and schedule data are missing.", Icons.Material.Filled.BuildCircle, Color.Warning),
new("2. Preisabweichung aktivieren", "2. Activate price variance", "Sehr hoher Managementnutzen, sobald EKPO Werte liefert.", "Very high management value as soon as EKPO provides values.", Icons.Material.Filled.TrendingUp, Color.Info),
new("3. Lieferantenrisiko aufbauen", "3. Build supplier risk", "Kombiniert Performance, offene Werte und Abhaengigkeit.", "Combines performance, open values and dependency.", Icons.Material.Filled.Security, Color.Info),
new("4. Contract Cockpit ausbauen", "4. Extend contract cockpit", "Mengenkontrakte und Restverpflichtungen brauchen EKET/EKPO.", "Quantity contracts and remaining commitments need EKET/EKPO.", Icons.Material.Filled.Assignment, Color.Info)
];
private IReadOnlyList<PurchasingIdeaKpi> PurchasingIdeaKpis =>
[
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("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("Working Capital", "Working capital", "Cash Forecast CHF", "cash forecast CHF", "Monat / Lieferant / Warengruppe", "EKPO+EKET+FI", "Konzept", "concept", Color.Info),
new("Datenqualitaet", "Data quality", "Mapping-Abdeckung %", "mapping coverage %", "Tabelle / Feld / Land", "EKKO+EKPO+Mapping", _liveState.EkkoLoaded ? "startklar" : "wartet", _liveState.EkkoLoaded ? "ready" : "waiting", _liveState.EkkoLoaded ? Color.Success : Color.Warning)
];
private readonly List<Purchasing3dIndicator> Purchasing3dIndicators =
[
new("spend", "Spend CHF", "Spend CHF", "CHF"),
@@ -661,6 +771,9 @@
private sealed record PurchasingSource(string Name, string Description);
private sealed record PurchasingPipelineStep(string TitleDe, string TitleEn, string Value, string DetailDe, string DetailEn, string Icon, bool IsReady, Color Color);
private sealed record PowerBiPageInfo(string Page, string Visuals, string Measure, string Dimensions);
private sealed record PurchasingIdea(string TitleDe, string TitleEn, string DescriptionDe, string DescriptionEn, string RequiredData, string ValueDe, string ValueEn, string StatusDe, string StatusEn, string Icon, Color Color);
private sealed record PurchasingIdeaPriority(string TitleDe, string TitleEn, string DetailDe, string DetailEn, string Icon, Color Color);
private sealed record PurchasingIdeaKpi(string AnalysisDe, string AnalysisEn, string KpiDe, string KpiEn, string Dimension, string Source, string StatusDe, string StatusEn, Color Color);
private sealed record Purchasing3dIndicator(string Key, string TitleDe, string TitleEn, string Unit);
private sealed record Purchasing3dBaseRow(string Axis, int Year, double Spend, double OpenValue, double OpenQuantity, double ContractValue, double SupplierScore);
}
@@ -933,6 +1046,87 @@
word-break: break-word;
}
.purchasing-idea-grid {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: 12px;
}
.purchasing-idea-card {
display: grid;
grid-template-columns: 46px minmax(0, 1fr);
gap: 12px;
padding: 14px;
border: 1px solid var(--mud-palette-lines-default);
border-radius: 8px;
background: var(--mud-palette-surface);
min-height: 170px;
}
.purchasing-idea-icon {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 8px;
background: rgba(21,101,192,.1);
}
.purchasing-idea-content {
display: flex;
flex-direction: column;
gap: 9px;
min-width: 0;
}
.purchasing-idea-title {
display: flex;
justify-content: space-between;
align-items: start;
gap: 10px;
}
.purchasing-idea-content > span,
.purchasing-idea-meta span,
.purchasing-priority-row span {
color: var(--mud-palette-text-secondary);
font-size: .88rem;
}
.purchasing-idea-meta {
display: grid;
gap: 6px;
margin-top: auto;
}
.purchasing-idea-meta code {
white-space: normal;
word-break: break-word;
}
.purchasing-priority-stack {
display: grid;
gap: 10px;
}
.purchasing-priority-row {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 10px;
align-items: start;
padding: 11px 0;
border-bottom: 1px solid var(--mud-palette-lines-default);
}
.purchasing-priority-row:last-child {
border-bottom: 0;
}
.purchasing-priority-row strong {
display: block;
margin-bottom: 4px;
}
.purchasing-source-row {
display: grid;
grid-template-columns: 26px minmax(0, 1fr);
@@ -965,6 +1159,7 @@
.purchasing-hero-metrics,
.purchasing-pipeline,
.purchasing-axis-grid,
.purchasing-idea-grid,
.purchasing-mini-donut-wrap {
grid-template-columns: 1fr;
}
@@ -0,0 +1,166 @@
REPORT ztest_powerbi_ekpo_eket.
" Kleines Diagnoseprogramm fuer die SAP Einkaufs-OData-Tabellen.
" Ziel:
" - Pruefen, ob EKKO, EKPO und EKET direkt in SAP Daten liefern.
" - Pruefen, ob zu einem EKKO-Beleg passende EKPO/EKET-Zeilen existieren.
" - Ausgabe per WRITE, damit sie einfach kopiert werden kann.
PARAMETERS:
p_ebeln TYPE ekko-ebeln,
p_bedat TYPE ekko-bedat DEFAULT '20260101',
p_max TYPE i DEFAULT 20.
DATA:
lv_ebeln TYPE ekko-ebeln,
lv_ekko_count TYPE i,
lv_ekpo_count TYPE i,
lv_eket_count TYPE i,
lt_ekko TYPE STANDARD TABLE OF ekko,
lt_ekpo TYPE STANDARD TABLE OF ekpo,
lt_eket TYPE STANDARD TABLE OF eket.
START-OF-SELECTION.
lv_ebeln = p_ebeln.
IF lv_ebeln IS NOT INITIAL.
lv_ebeln = |{ lv_ebeln ALPHA = IN }|.
ENDIF.
WRITE: / '=== Einkaufsdaten Test EKKO / EKPO / EKET ==='.
WRITE: / 'Input EBELN:', p_ebeln, 'ALPHA:', lv_ebeln.
WRITE: / 'Input BEDAT ab:', p_bedat.
WRITE: / 'Max Zeilen:', p_max.
ULINE.
" 1) Grundzaehlung ohne Join
SELECT COUNT( * )
FROM ekko
INTO @lv_ekko_count
WHERE bedat >= @p_bedat.
SELECT COUNT( * )
FROM ekpo
INTO @lv_ekpo_count.
SELECT COUNT( * )
FROM eket
INTO @lv_eket_count.
WRITE: / 'COUNT EKKO ab BEDAT:', lv_ekko_count.
WRITE: / 'COUNT EKPO gesamt :', lv_ekpo_count.
WRITE: / 'COUNT EKET gesamt :', lv_eket_count.
ULINE.
" 2) Beispiel-EKKO suchen, falls kein Beleg mitgegeben wurde
IF lv_ebeln IS INITIAL.
SELECT *
FROM ekko
WHERE bedat >= @p_bedat
ORDER BY bedat DESCENDING, ebeln DESCENDING
INTO TABLE @lt_ekko
UP TO 1 ROWS.
READ TABLE lt_ekko INDEX 1 INTO DATA(ls_first_ekko).
IF sy-subrc = 0.
lv_ebeln = ls_first_ekko-ebeln.
WRITE: / 'Kein EBELN mitgegeben, verwende ersten EKKO-Beleg:', lv_ebeln.
ELSE.
WRITE: / 'Kein EKKO-Beleg ab BEDAT gefunden.'.
RETURN.
ENDIF.
ENDIF.
ULINE.
WRITE: / '=== Detailtest fuer EBELN ===', lv_ebeln.
" 3) EKKO Detail
CLEAR lt_ekko.
SELECT *
FROM ekko
WHERE ebeln = @lv_ebeln
INTO TABLE @lt_ekko
UP TO @p_max ROWS.
WRITE: / 'EKKO Zeilen fuer EBELN:', lines( lt_ekko ).
LOOP AT lt_ekko INTO DATA(ls_ekko).
WRITE: / 'EKKO',
'EBELN=', ls_ekko-ebeln,
'BEDAT=', ls_ekko-bedat,
'AEDAT=', ls_ekko-aedat,
'LIFNR=', ls_ekko-lifnr,
'BUKRS=', ls_ekko-bukrs,
'BSART=', ls_ekko-bsart.
ENDLOOP.
ULINE.
" 4) EKPO Detail
CLEAR lt_ekpo.
SELECT *
FROM ekpo
WHERE ebeln = @lv_ebeln
ORDER BY ebeln, ebelp
INTO TABLE @lt_ekpo
UP TO @p_max ROWS.
WRITE: / 'EKPO Zeilen fuer EBELN:', lines( lt_ekpo ).
LOOP AT lt_ekpo INTO DATA(ls_ekpo).
WRITE: / 'EKPO',
'EBELN=', ls_ekpo-ebeln,
'EBELP=', ls_ekpo-ebelp,
'MATNR=', ls_ekpo-matnr,
'MATKL=', ls_ekpo-matkl,
'MENGE=', ls_ekpo-menge,
'MEINS=', ls_ekpo-meins,
'NETWR=', ls_ekpo-netwr,
'LOEKZ=', ls_ekpo-loekz.
ENDLOOP.
ULINE.
" 5) EKET Detail
CLEAR lt_eket.
SELECT *
FROM eket
WHERE ebeln = @lv_ebeln
ORDER BY ebeln, ebelp, etenr
INTO TABLE @lt_eket
UP TO @p_max ROWS.
WRITE: / 'EKET Zeilen fuer EBELN:', lines( lt_eket ).
LOOP AT lt_eket INTO DATA(ls_eket).
WRITE: / 'EKET',
'EBELN=', ls_eket-ebeln,
'EBELP=', ls_eket-ebelp,
'ETENR=', ls_eket-etenr,
'EINDT=', ls_eket-eindt,
'MENGE=', ls_eket-menge,
'WEMNG=', ls_eket-wemng.
ENDLOOP.
ULINE.
" 6) Join-Pruefung: existieren EKPO/EKET zu den aktuellen EKKO-Belegen?
SELECT COUNT( * )
FROM ekko AS h
INNER JOIN ekpo AS p
ON p~ebeln = h~ebeln
INTO @lv_ekpo_count
WHERE h~bedat >= @p_bedat.
SELECT COUNT( * )
FROM ekko AS h
INNER JOIN eket AS e
ON e~ebeln = h~ebeln
INTO @lv_eket_count
WHERE h~bedat >= @p_bedat.
WRITE: / 'JOIN EKKO->EKPO ab BEDAT:', lv_ekpo_count.
WRITE: / 'JOIN EKKO->EKET ab BEDAT:', lv_eket_count.
ULINE.
WRITE: / 'Interpretation:'.
WRITE: / '- Wenn EKPO/EKET gesamt > 0, aber fuer EBELN 0: Beleg hat keine Positionen/Termine oder falscher Beleg.'.
WRITE: / '- Wenn JOIN ab BEDAT > 0, dann muss OData EKPO/EKET mit korrektem SELECT auch Daten liefern.'.
WRITE: / '- Wenn JOIN ab BEDAT = 0, dann gibt es fuer aktuelle EKKO-Belege keine EKPO/EKET-Zuordnung im getesteten Zeitraum.'.