Show purchasing commitments with supplier context
This commit is contained in:
@@ -139,10 +139,10 @@
|
|||||||
case "kontrakte":
|
case "kontrakte":
|
||||||
<PurchasingSection TitleDe="Offene Verpflichtungen"
|
<PurchasingSection TitleDe="Offene Verpflichtungen"
|
||||||
TitleEn="Open commitments"
|
TitleEn="Open commitments"
|
||||||
DescriptionDe="Kontrakte und Restverpflichtungen werden auf EKPO/EKET aufgebaut. Der Bereich zeigt bereits die Zielkennzahlen und den aktuellen Ladezustand."
|
DescriptionDe="Restverpflichtungen werden aus EKET-Offenmenge und EKPO-Stueckwert berechnet und nach Lieferant, Artikel und Faelligkeitsmonat gezeigt."
|
||||||
DescriptionEn="Contracts and remaining commitments are built on EKPO/EKET. This area already shows the target KPIs and current load status."
|
DescriptionEn="Remaining commitments are calculated from EKET open quantity and EKPO unit value and shown by supplier, article and due month."
|
||||||
ChartTitleDe="Kontrakt- und Verpflichtungsuebersicht"
|
ChartTitleDe="Top Verpflichtungen nach Lieferant, Artikel und Faelligkeit"
|
||||||
ChartTitleEn="Contract and commitment overview"
|
ChartTitleEn="Top commitments by supplier, article and due date"
|
||||||
Kpis="@ContractKpis"
|
Kpis="@ContractKpis"
|
||||||
ChartRows="@ContractChartRows"
|
ChartRows="@ContractChartRows"
|
||||||
StatusRows="@ContractStatusRows"
|
StatusRows="@ContractStatusRows"
|
||||||
@@ -686,7 +686,7 @@
|
|||||||
[
|
[
|
||||||
new("Spend total", "Total spend", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : T("wartet auf EKPO", "waiting for EKPO"), _liveState.UsesCache ? "Einkauf Cache Vollwerte" : _liveState.EkpoLoaded ? "EKPO-Live-Sample" : "EKKO live, Positionswerte fehlen noch", _liveState.UsesCache ? "purchasing cache full values" : _liveState.EkpoLoaded ? "EKPO live sample" : "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.UsesCache ? "Einkauf Cache Vollwerte" : _liveState.EkpoLoaded ? "EKPO-Live-Sample" : "EKKO live, Positionswerte fehlen noch", _liveState.UsesCache ? "purchasing cache full values" : _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 im Zeitraum {FilterLabel}" : "Noch nicht geladen", _liveState.EkkoLoaded ? $"EKKO orders in period {FilterLabel}" : "Not loaded yet", Icons.Material.Filled.PendingActions, Color.Warning),
|
new("Offene Bestellungen", "Open orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", _liveState.EkkoLoaded ? $"EKKO-Belege im Zeitraum {FilterLabel}" : "Noch nicht geladen", _liveState.EkkoLoaded ? $"EKKO orders in period {FilterLabel}" : "Not loaded yet", Icons.Material.Filled.PendingActions, Color.Warning),
|
||||||
new("Kontrakte", "Contracts", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : T("wartet auf EKET", "waiting for EKET"), _liveState.UsesCache ? "Restwert aus Einkauf Cache" : _liveState.EketLoaded ? "Restwert aus EKET/EKPO-Sample" : "EKKO live, Terminwerte fehlen noch", _liveState.UsesCache ? "Remaining value from purchasing cache" : _liveState.EketLoaded ? "Remaining value from EKET/EKPO sample" : "EKKO live, schedule values still missing", Icons.Material.Filled.Assignment, Color.Info),
|
new("Verpflichtungen", "Commitments", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : T("wartet auf EKET", "waiting for EKET"), _liveState.UsesCache ? "nach Lieferant / Artikel / Faelligkeit" : _liveState.EketLoaded ? "Restwert aus EKET/EKPO-Sample" : "EKKO live, Terminwerte fehlen noch", _liveState.UsesCache ? "by supplier / article / due date" : _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)
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -774,7 +774,7 @@
|
|||||||
[
|
[
|
||||||
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("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("Top Verpflichtung", "Top commitment", TopCommitmentLabel, "Lieferant / Artikel / Monat", "supplier / article / month"),
|
||||||
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")
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1396,7 +1396,7 @@
|
|||||||
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 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, "Artikel / Lieferant / Monat", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP")
|
||||||
];
|
];
|
||||||
|
|
||||||
private IReadOnlyList<PurchasingSectionDetailRow> OpenOrderDetailRows =>
|
private IReadOnlyList<PurchasingSectionDetailRow> OpenOrderDetailRows =>
|
||||||
@@ -1409,10 +1409,10 @@
|
|||||||
|
|
||||||
private IReadOnlyList<PurchasingSectionDetailRow> ContractDetailRows =>
|
private IReadOnlyList<PurchasingSectionDetailRow> ContractDetailRows =>
|
||||||
[
|
[
|
||||||
new("Restverpflichtung", "Remaining commitment", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : 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)), "EKET offen * EKPO Stueckwert", _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("Groesste Verpflichtung", "Largest commitment", TopCommitmentLabel, "Lieferant / Artikel / Faelligkeit", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP"),
|
||||||
new("Abrufquote", "Consumption rate", "offen", "Kontraktmenge / Abrufmenge", "Wartet auf SAP"),
|
new("Faelligkeitsverlauf", "Due-date trend", _liveState.OpenValueChartRows.Count > 0 ? $"{_liveState.OpenValueChartRows.Count:N0} Monate" : "-", "eketSet.Eindt", _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")
|
new("Namensmapping", "Name mapping", _liveState.EketLoaded ? "Lifnr sichtbar, Name offen" : "wartet", "Data/LFA1 fehlt im OData", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
|
||||||
];
|
];
|
||||||
|
|
||||||
private IReadOnlyList<PurchasingSectionDetailRow> SupplierDetailRows =>
|
private IReadOnlyList<PurchasingSectionDetailRow> SupplierDetailRows =>
|
||||||
@@ -1530,6 +1530,7 @@
|
|||||||
private string TopSpendLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopSupplierLabel) ? _liveState.TopSupplierLabel : BuildTopLabel(x => x.Spend, FormatChf);
|
private string TopSpendLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopSupplierLabel) ? _liveState.TopSupplierLabel : BuildTopLabel(x => x.Spend, FormatChf);
|
||||||
private string TopMaterialGroupLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopMaterialGroupLabel) ? _liveState.TopMaterialGroupLabel : BuildTopLabel(x => x.Spend, FormatChf, "Warengruppe");
|
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 TopArticleLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopArticleLabel) ? _liveState.TopArticleLabel : BuildTopLabel(x => x.Spend, FormatChf, "Artikel");
|
||||||
|
private string TopCommitmentLabel => _liveState.EketLoaded && !string.IsNullOrWhiteSpace(_liveState.TopCommitmentLabel) ? _liveState.TopCommitmentLabel : "-";
|
||||||
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...")
|
||||||
|
|||||||
@@ -202,6 +202,12 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.purchasing-bar-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
@@media (max-width: 760px) {
|
@@media (max-width: 760px) {
|
||||||
.purchasing-bar-row,
|
.purchasing-bar-row,
|
||||||
.purchasing-status-row,
|
.purchasing-status-row,
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ public sealed class PurchasingDashboardLiveState
|
|||||||
public string TopSupplierLabel { get; set; } = string.Empty;
|
public string TopSupplierLabel { get; set; } = string.Empty;
|
||||||
public string TopMaterialGroupLabel { get; set; } = string.Empty;
|
public string TopMaterialGroupLabel { get; set; } = string.Empty;
|
||||||
public string TopArticleLabel { get; set; } = string.Empty;
|
public string TopArticleLabel { get; set; } = string.Empty;
|
||||||
|
public string TopCommitmentLabel { get; set; } = string.Empty;
|
||||||
public List<PurchasingLiveChartPoint> SpendChartRows { get; set; } = [];
|
public List<PurchasingLiveChartPoint> SpendChartRows { get; set; } = [];
|
||||||
public List<PurchasingLiveChartPoint> OpenValueChartRows { get; set; } = [];
|
public List<PurchasingLiveChartPoint> OpenValueChartRows { get; set; } = [];
|
||||||
public List<PurchasingLiveChartPoint> ContractChartRows { get; set; } = [];
|
public List<PurchasingLiveChartPoint> ContractChartRows { get; set; } = [];
|
||||||
|
public List<PurchasingLiveChartPoint> CommitmentDetailChartRows { get; set; } = [];
|
||||||
public List<PurchasingLiveChartPoint> DeliveryRiskChartRows { get; set; } = [];
|
public List<PurchasingLiveChartPoint> DeliveryRiskChartRows { get; set; } = [];
|
||||||
public List<PurchasingLiveChartPoint> PriceVarianceChartRows { get; set; } = [];
|
public List<PurchasingLiveChartPoint> PriceVarianceChartRows { get; set; } = [];
|
||||||
public List<PurchasingLiveChartPoint> SpendConcentrationChartRows { get; set; } = [];
|
public List<PurchasingLiveChartPoint> SpendConcentrationChartRows { get; set; } = [];
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
|
|||||||
return new PurchasingDashboardFilter(new DateTime(today.Year - 2, 1, 1), today);
|
return new PurchasingDashboardFilter(new DateTime(today.Year - 2, 1, 1), today);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SupplierLabelSql(string lifnrExpression)
|
||||||
|
=> $@"CASE
|
||||||
|
WHEN COALESCE(NULLIF({lifnrExpression}, ''), '') = '' THEN 'ohne Lieferant'
|
||||||
|
ELSE 'Lief. ' || {lifnrExpression} || ' (Name fehlt)'
|
||||||
|
END";
|
||||||
|
|
||||||
public async Task<PurchasingDashboardLiveState> LoadAsync(PurchasingDashboardFilter? filter = null, CancellationToken cancellationToken = default)
|
public async Task<PurchasingDashboardLiveState> LoadAsync(PurchasingDashboardFilter? filter = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var state = new PurchasingDashboardLiveState();
|
var state = new PurchasingDashboardLiveState();
|
||||||
@@ -100,7 +106,7 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
|
|||||||
state.EketLoaded = eketRows.Count > 0;
|
state.EketLoaded = eketRows.Count > 0;
|
||||||
|
|
||||||
ApplyEkpoMetrics(state, ekkoRows, ekpoRows);
|
ApplyEkpoMetrics(state, ekkoRows, ekpoRows);
|
||||||
ApplyEketMetrics(state, ekpoRows, eketRows);
|
ApplyEketMetrics(state, ekkoRows, ekpoRows, eketRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Message = state.EkpoLoaded && state.EketLoaded
|
state.Message = state.EkpoLoaded && state.EketLoaded
|
||||||
@@ -164,7 +170,7 @@ LEFT JOIN PurchasingEkpoCache p ON p.Ebeln = e.Ebeln AND p.Ebelp = e.Ebelp
|
|||||||
WHERE COALESCE(p.Loekz, '') = '' AND " + eketPeriod + ";", cancellationToken);
|
WHERE COALESCE(p.Loekz, '') = '' AND " + eketPeriod + ";", cancellationToken);
|
||||||
state.ContractValueSample = state.OpenValueSample;
|
state.ContractValueSample = state.OpenValueSample;
|
||||||
state.TopSupplierLabel = await ExecuteTopLabelAsync(conn, @"
|
state.TopSupplierLabel = await ExecuteTopLabelAsync(conn, @"
|
||||||
SELECT COALESCE(k.Lifnr, 'ohne Lieferant') AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value
|
SELECT " + SupplierLabelSql("k.Lifnr") + @" AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value
|
||||||
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 " + joinedEkkoPeriod + @"
|
WHERE COALESCE(p.Loekz, '') = '' AND " + joinedEkkoPeriod + @"
|
||||||
@@ -180,15 +186,19 @@ GROUP BY COALESCE(NULLIF(Matkl, ''), 'ohne Warengruppe')
|
|||||||
ORDER BY Value DESC
|
ORDER BY Value DESC
|
||||||
LIMIT 1;", "Warengruppe", cancellationToken);
|
LIMIT 1;", "Warengruppe", cancellationToken);
|
||||||
state.TopArticleLabel = await ExecuteTopLabelAsync(conn, @"
|
state.TopArticleLabel = await ExecuteTopLabelAsync(conn, @"
|
||||||
SELECT COALESCE(NULLIF(Matnr, ''), NULLIF(Txz01, ''), 'ohne Artikel') AS Label, SUM(CAST(Netwr AS REAL)) AS Value
|
SELECT
|
||||||
|
COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') || ' | ' ||
|
||||||
|
" + SupplierLabelSql("k.Lifnr") + @" || ' | Monat ' ||
|
||||||
|
COALESCE(substr(k.Bedat, 1, 7), 'ohne Datum') AS Label,
|
||||||
|
SUM(CAST(p.Netwr AS REAL)) AS Value
|
||||||
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 " + joinedEkkoPeriod + @"
|
WHERE COALESCE(p.Loekz, '') = '' AND " + joinedEkkoPeriod + @"
|
||||||
GROUP BY COALESCE(NULLIF(Matnr, ''), NULLIF(Txz01, ''), 'ohne Artikel')
|
GROUP BY COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel'), COALESCE(k.Lifnr, 'ohne Lieferant'), COALESCE(substr(k.Bedat, 1, 7), 'ohne Datum')
|
||||||
ORDER BY Value DESC
|
ORDER BY Value DESC
|
||||||
LIMIT 1;", "Artikel", cancellationToken);
|
LIMIT 1;", "Artikel", cancellationToken);
|
||||||
state.SpendChartRows = await ExecuteChartRowsAsync(conn, @"
|
state.SpendChartRows = await ExecuteChartRowsAsync(conn, @"
|
||||||
SELECT 'Lieferant ' || COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant') AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value
|
SELECT " + SupplierLabelSql("k.Lifnr") + @" AS Label, SUM(CAST(p.Netwr AS REAL)) AS Value
|
||||||
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 " + joinedEkkoPeriod + @"
|
WHERE COALESCE(p.Loekz, '') = '' AND " + joinedEkkoPeriod + @"
|
||||||
@@ -205,7 +215,26 @@ WHERE COALESCE(p.Loekz, '') = '' AND " + eketPeriod + @"
|
|||||||
GROUP BY COALESCE(substr(e.Eindt, 1, 7), 'ohne Termin')
|
GROUP BY COALESCE(substr(e.Eindt, 1, 7), 'ohne Termin')
|
||||||
ORDER BY Label
|
ORDER BY Label
|
||||||
LIMIT 6;", cancellationToken);
|
LIMIT 6;", cancellationToken);
|
||||||
state.ContractChartRows = state.OpenValueChartRows.ToList();
|
state.CommitmentDetailChartRows = await ExecuteChartRowsAsync(conn, @"
|
||||||
|
SELECT
|
||||||
|
" + SupplierLabelSql("k.Lifnr") + @" || ' | ' ||
|
||||||
|
COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel') || ' | faellig ' ||
|
||||||
|
COALESCE(substr(e.Eindt, 1, 7), 'ohne Termin') AS Label,
|
||||||
|
SUM(MAX(CAST(e.Menge AS REAL) - CAST(e.Wemng AS REAL), 0) *
|
||||||
|
CASE WHEN CAST(p.Menge AS REAL) = 0 THEN 0 ELSE CAST(p.Netwr AS REAL) / CAST(p.Menge AS REAL) END) AS Value
|
||||||
|
FROM PurchasingEketCache e
|
||||||
|
LEFT JOIN PurchasingEkpoCache p ON p.Ebeln = e.Ebeln AND p.Ebelp = e.Ebelp
|
||||||
|
LEFT JOIN PurchasingEkkoCache k ON k.Ebeln = e.Ebeln
|
||||||
|
WHERE COALESCE(p.Loekz, '') = '' AND " + eketPeriod + @" AND MAX(CAST(e.Menge AS REAL) - CAST(e.Wemng AS REAL), 0) > 0
|
||||||
|
GROUP BY COALESCE(NULLIF(k.Lifnr, ''), 'ohne Lieferant'), COALESCE(NULLIF(p.Matnr, ''), NULLIF(p.Txz01, ''), 'ohne Artikel'), COALESCE(substr(e.Eindt, 1, 7), 'ohne Termin')
|
||||||
|
ORDER BY Value DESC
|
||||||
|
LIMIT 6;", cancellationToken);
|
||||||
|
state.ContractChartRows = state.CommitmentDetailChartRows.Count > 0
|
||||||
|
? state.CommitmentDetailChartRows.ToList()
|
||||||
|
: state.OpenValueChartRows.ToList();
|
||||||
|
state.TopCommitmentLabel = state.CommitmentDetailChartRows.Count > 0
|
||||||
|
? $"{state.CommitmentDetailChartRows[0].Label}: CHF {state.CommitmentDetailChartRows[0].Value:N0}"
|
||||||
|
: string.Empty;
|
||||||
await ApplyIdeaAnalyticsAsync(conn, state, joinedEkkoPeriod, eketPeriod, cancellationToken);
|
await ApplyIdeaAnalyticsAsync(conn, state, joinedEkkoPeriod, eketPeriod, cancellationToken);
|
||||||
state.CacheStatus = latestStatus.Status;
|
state.CacheStatus = latestStatus.Status;
|
||||||
state.CacheCompletedAtUtc = latestStatus.CompletedAtUtc;
|
state.CacheCompletedAtUtc = latestStatus.CompletedAtUtc;
|
||||||
@@ -349,22 +378,29 @@ SELECT 'Nullwert', COUNT(*) || ' Positionen', 'EKPO.Netwr = 0', CASE WHEN COUNT(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var supplierByEbeln = ekkoRows
|
var supplierByEbeln = ekkoRows
|
||||||
.Select(row => new { Ebeln = GetText(row, "Ebeln"), Lifnr = GetText(row, "Lifnr") })
|
.Select(row => new { Ebeln = GetText(row, "Ebeln"), Lifnr = FormatSupplierLabel(GetText(row, "Lifnr")) })
|
||||||
.Where(row => !string.IsNullOrWhiteSpace(row.Ebeln))
|
.Where(row => !string.IsNullOrWhiteSpace(row.Ebeln))
|
||||||
.GroupBy(row => row.Ebeln, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(row => row.Ebeln, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(group => group.Key, group => group.First().Lifnr, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(group => group.Key, group => group.First().Lifnr, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var monthByEbeln = ekkoRows
|
||||||
|
.Select(row => new { Ebeln = GetText(row, "Ebeln"), Month = TryParseSapDate(GetText(row, "Bedat"))?.ToString("yyyy-MM") ?? "ohne Datum" })
|
||||||
|
.Where(row => !string.IsNullOrWhiteSpace(row.Ebeln))
|
||||||
|
.GroupBy(row => row.Ebeln, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(group => group.Key, group => group.First().Month, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var enriched = ekpoRows
|
var enriched = ekpoRows
|
||||||
.Select(row =>
|
.Select(row =>
|
||||||
{
|
{
|
||||||
var ebeln = GetText(row, "Ebeln");
|
var ebeln = GetText(row, "Ebeln");
|
||||||
supplierByEbeln.TryGetValue(ebeln, out var supplier);
|
supplierByEbeln.TryGetValue(ebeln, out var supplier);
|
||||||
|
monthByEbeln.TryGetValue(ebeln, out var month);
|
||||||
var netwr = GetDecimal(row, "Netwr");
|
var netwr = GetDecimal(row, "Netwr");
|
||||||
var quantity = GetDecimal(row, "Menge");
|
var quantity = GetDecimal(row, "Menge");
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
Ebeln = ebeln,
|
Ebeln = ebeln,
|
||||||
Supplier = string.IsNullOrWhiteSpace(supplier) ? "ohne Lieferant" : supplier,
|
Supplier = string.IsNullOrWhiteSpace(supplier) ? "ohne Lieferant" : supplier,
|
||||||
|
Month = string.IsNullOrWhiteSpace(month) ? "ohne Datum" : month,
|
||||||
Material = FirstNonEmpty(GetText(row, "Matnr"), GetText(row, "Txz01"), "ohne Artikel"),
|
Material = FirstNonEmpty(GetText(row, "Matnr"), GetText(row, "Txz01"), "ohne Artikel"),
|
||||||
MaterialGroup = FirstNonEmpty(GetText(row, "Matkl"), "ohne Warengruppe"),
|
MaterialGroup = FirstNonEmpty(GetText(row, "Matkl"), "ohne Warengruppe"),
|
||||||
NetValue = netwr,
|
NetValue = netwr,
|
||||||
@@ -376,10 +412,10 @@ SELECT 'Nullwert', COUNT(*) || ' Positionen', 'EKPO.Netwr = 0', CASE WHEN COUNT(
|
|||||||
state.SpendChfSample = enriched.Sum(row => row.NetValue);
|
state.SpendChfSample = enriched.Sum(row => row.NetValue);
|
||||||
state.TopSupplierLabel = BuildTopLabel(enriched.GroupBy(row => row.Supplier), row => row.NetValue, "Lieferant");
|
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.TopMaterialGroupLabel = BuildTopLabel(enriched.GroupBy(row => row.MaterialGroup), row => row.NetValue, "Warengruppe");
|
||||||
state.TopArticleLabel = BuildTopLabel(enriched.GroupBy(row => row.Material), row => row.NetValue, "Artikel");
|
state.TopArticleLabel = BuildTopLabel(enriched.GroupBy(row => $"{row.Material} | {row.Supplier} | Monat {row.Month}"), row => row.NetValue, "Artikel");
|
||||||
state.SpendChartRows = enriched
|
state.SpendChartRows = enriched
|
||||||
.GroupBy(row => row.Supplier)
|
.GroupBy(row => row.Supplier)
|
||||||
.Select(group => new PurchasingLiveChartPoint($"Lief. {group.Key}", group.Sum(row => row.NetValue)))
|
.Select(group => new PurchasingLiveChartPoint(group.Key, group.Sum(row => row.NetValue)))
|
||||||
.OrderByDescending(row => row.Value)
|
.OrderByDescending(row => row.Value)
|
||||||
.Take(6)
|
.Take(6)
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -387,12 +423,31 @@ SELECT 'Nullwert', COUNT(*) || ' Positionen', 'EKPO.Netwr = 0', CASE WHEN COUNT(
|
|||||||
|
|
||||||
private static void ApplyEketMetrics(
|
private static void ApplyEketMetrics(
|
||||||
PurchasingDashboardLiveState state,
|
PurchasingDashboardLiveState state,
|
||||||
|
List<Dictionary<string, object?>> ekkoRows,
|
||||||
List<Dictionary<string, object?>> ekpoRows,
|
List<Dictionary<string, object?>> ekpoRows,
|
||||||
List<Dictionary<string, object?>> eketRows)
|
List<Dictionary<string, object?>> eketRows)
|
||||||
{
|
{
|
||||||
if (eketRows.Count == 0)
|
if (eketRows.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var supplierByEbeln = ekkoRows
|
||||||
|
.Select(row => new { Ebeln = GetText(row, "Ebeln"), Lifnr = FormatSupplierLabel(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 itemByPosition = ekpoRows
|
||||||
|
.Select(row =>
|
||||||
|
{
|
||||||
|
var ebeln = GetText(row, "Ebeln");
|
||||||
|
var ebelp = GetText(row, "Ebelp");
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
key = $"{ebeln}|{ebelp}",
|
||||||
|
Article = FirstNonEmpty(GetText(row, "Matnr"), GetText(row, "Txz01"), "ohne Artikel")
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.GroupBy(row => row.key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(group => group.Key, group => group.First().Article, StringComparer.OrdinalIgnoreCase);
|
||||||
var netPriceByPosition = ekpoRows
|
var netPriceByPosition = ekpoRows
|
||||||
.Select(row =>
|
.Select(row =>
|
||||||
{
|
{
|
||||||
@@ -414,12 +469,16 @@ SELECT 'Nullwert', COUNT(*) || ' Positionen', 'EKPO.Netwr = 0', CASE WHEN COUNT(
|
|||||||
var ebelp = GetText(row, "Ebelp");
|
var ebelp = GetText(row, "Ebelp");
|
||||||
var key = $"{ebeln}|{ebelp}";
|
var key = $"{ebeln}|{ebelp}";
|
||||||
netPriceByPosition.TryGetValue(key, out var netPrice);
|
netPriceByPosition.TryGetValue(key, out var netPrice);
|
||||||
|
itemByPosition.TryGetValue(key, out var article);
|
||||||
|
supplierByEbeln.TryGetValue(ebeln, out var supplier);
|
||||||
var quantity = GetDecimal(row, "Menge");
|
var quantity = GetDecimal(row, "Menge");
|
||||||
var received = GetDecimal(row, "Wemng");
|
var received = GetDecimal(row, "Wemng");
|
||||||
var openQuantity = Math.Max(0, quantity - received);
|
var openQuantity = Math.Max(0, quantity - received);
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
Ebeln = ebeln,
|
Ebeln = ebeln,
|
||||||
|
Supplier = string.IsNullOrWhiteSpace(supplier) ? "ohne Lieferant" : supplier,
|
||||||
|
Article = string.IsNullOrWhiteSpace(article) ? "ohne Artikel" : article,
|
||||||
DueDate = TryParseSapDate(GetText(row, "Eindt")),
|
DueDate = TryParseSapDate(GetText(row, "Eindt")),
|
||||||
OpenQuantity = openQuantity,
|
OpenQuantity = openQuantity,
|
||||||
OpenValue = openQuantity * netPrice
|
OpenValue = openQuantity * netPrice
|
||||||
@@ -436,7 +495,19 @@ SELECT 'Nullwert', COUNT(*) || ' Positionen', 'EKPO.Netwr = 0', CASE WHEN COUNT(
|
|||||||
.OrderBy(row => row.Label)
|
.OrderBy(row => row.Label)
|
||||||
.Take(6)
|
.Take(6)
|
||||||
.ToList();
|
.ToList();
|
||||||
state.ContractChartRows = state.OpenValueChartRows.ToList();
|
state.CommitmentDetailChartRows = enriched
|
||||||
|
.Where(row => row.OpenValue > 0)
|
||||||
|
.GroupBy(row => $"{row.Supplier} | {row.Article} | faellig {row.DueDate?.ToString("yyyy-MM") ?? "ohne Termin"}")
|
||||||
|
.Select(group => new PurchasingLiveChartPoint(group.Key, group.Sum(row => row.OpenValue)))
|
||||||
|
.OrderByDescending(row => row.Value)
|
||||||
|
.Take(6)
|
||||||
|
.ToList();
|
||||||
|
state.ContractChartRows = state.CommitmentDetailChartRows.Count > 0
|
||||||
|
? state.CommitmentDetailChartRows.ToList()
|
||||||
|
: state.OpenValueChartRows.ToList();
|
||||||
|
state.TopCommitmentLabel = state.CommitmentDetailChartRows.Count > 0
|
||||||
|
? $"{state.CommitmentDetailChartRows[0].Label}: CHF {state.CommitmentDetailChartRows[0].Value:N0}"
|
||||||
|
: string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpClient CreateClient(string username, string password)
|
private static HttpClient CreateClient(string username, string password)
|
||||||
@@ -597,6 +668,11 @@ SELECT 'Nullwert', COUNT(*) || ' Positionen', 'EKPO.Netwr = 0', CASE WHEN COUNT(
|
|||||||
private static string FirstNonEmpty(params string[] values)
|
private static string FirstNonEmpty(params string[] values)
|
||||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
|
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
|
||||||
|
|
||||||
|
private static string FormatSupplierLabel(string supplierNumber)
|
||||||
|
=> string.IsNullOrWhiteSpace(supplierNumber)
|
||||||
|
? "ohne Lieferant"
|
||||||
|
: $"Lief. {supplierNumber} (Name fehlt)";
|
||||||
|
|
||||||
private static string BuildTopLabel<T>(IEnumerable<IGrouping<string, T>> groups, Func<T, decimal> selector, string fallback)
|
private static string BuildTopLabel<T>(IEnumerable<IGrouping<string, T>> groups, Func<T, decimal> selector, string fallback)
|
||||||
{
|
{
|
||||||
var top = groups
|
var top = groups
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ Die Seite `/einkauf` zeigt nun echte Werte aus dem SAP-Cache:
|
|||||||
- `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.
|
||||||
|
- Top-Artikel zeigt nun Artikel, Lieferant und Bestellmonat, damit ein Wert wie `C42698: CHF 1` fachlich nachvollziehbar ist.
|
||||||
|
- Die Verpflichtungs-/Kontraktseite zeigt Top-Restverpflichtungen nach Lieferant, Artikel und Faelligkeitsmonat, nicht nur den Monatsverlauf.
|
||||||
- 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`.
|
- Der Standardzeitraum ist rollierend auf die letzten drei Kalenderjahre bis heute gesetzt. Die Datumsabgrenzung erfolgt im Dashboard ueber `Von Monat` und `Bis Monat`.
|
||||||
@@ -180,7 +182,7 @@ 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`.
|
- 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`.
|
- Der aktuelle SAP-OData-Service liefert produktiv `EKKOSet`, `EKPOSet` und `eketSet`.
|
||||||
- Tests auf `Data`, `Data2`, `DataSet` und `Data2Set` liefern aktuell `404 Resource not found`.
|
- 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.
|
- Bis diese Mapping-Quelle angebunden ist, zeigt das Dashboard Lieferanten als `Lief. <Nummer> (Name fehlt)` und Warengruppen-Codes statt vollstaendiger Namen.
|
||||||
|
|
||||||
## Ideen und Kennzahlen-Katalog
|
## Ideen und Kennzahlen-Katalog
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user