Load purchasing EKPO and EKET metrics

This commit is contained in:
2026-06-05 11:05:04 +02:00
parent e7e408fc20
commit 2fa410ec31
5 changed files with 239 additions and 30 deletions
@@ -74,22 +74,27 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
{
var ekpoRows = await ReadRowsAsync(
client,
$"{baseUrl}EKPOSet?$format=json&$top=50&$filter={Uri.EscapeDataString($"Ebeln eq '{firstEbeln}'")}",
$"{baseUrl}EKPOSet?$format=json&$top=1000&$filter={Uri.EscapeDataString($"Ebeln ge '{firstEbeln}'")}",
cancellationToken);
state.PositionSampleCount = ekpoRows.Count;
state.EkpoLoaded = ekpoRows.Count > 0;
var eketRows = await ReadRowsAsync(
client,
$"{baseUrl}eketSet?$format=json&$top=50&$filter={Uri.EscapeDataString($"Ebeln eq '{firstEbeln}'")}",
$"{baseUrl}eketSet?$format=json&$top=1000&$filter={Uri.EscapeDataString($"Ebeln ge '{firstEbeln}'")}",
cancellationToken);
state.ScheduleSampleCount = eketRows.Count;
state.EketLoaded = eketRows.Count > 0;
ApplyEkpoMetrics(state, ekkoRows, ekpoRows);
ApplyEketMetrics(state, ekpoRows, eketRows);
}
state.Message = state.EkpoLoaded
? "SAP Einkaufsdaten geladen."
: "EKKO ist live geladen; EKPO/EKET liefern aktuell noch keine Positionsdaten.";
state.Message = state.EkpoLoaded && state.EketLoaded
? "SAP Einkaufsdaten inkl. EKPO/EKET geladen."
: state.EkpoLoaded
? "SAP Einkaufsdaten inkl. EKPO geladen; EKET liefert noch keine Termindaten."
: "EKKO ist live geladen; EKPO/EKET liefern aktuell noch keine Positionsdaten.";
}
catch (Exception ex)
{
@@ -99,6 +104,105 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
return state;
}
private static void ApplyEkpoMetrics(
PurchasingDashboardLiveState state,
List<Dictionary<string, object?>> ekkoRows,
List<Dictionary<string, object?>> ekpoRows)
{
if (ekpoRows.Count == 0)
return;
var supplierByEbeln = ekkoRows
.Select(row => new { Ebeln = GetText(row, "Ebeln"), Lifnr = GetText(row, "Lifnr") })
.Where(row => !string.IsNullOrWhiteSpace(row.Ebeln))
.GroupBy(row => row.Ebeln, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First().Lifnr, StringComparer.OrdinalIgnoreCase);
var enriched = ekpoRows
.Select(row =>
{
var ebeln = GetText(row, "Ebeln");
supplierByEbeln.TryGetValue(ebeln, out var supplier);
var netwr = GetDecimal(row, "Netwr");
var quantity = GetDecimal(row, "Menge");
return new
{
Ebeln = ebeln,
Supplier = string.IsNullOrWhiteSpace(supplier) ? "ohne Lieferant" : supplier,
Material = FirstNonEmpty(GetText(row, "Matnr"), GetText(row, "Txz01"), "ohne Artikel"),
MaterialGroup = FirstNonEmpty(GetText(row, "Matkl"), "ohne Warengruppe"),
NetValue = netwr,
Quantity = quantity
};
})
.ToList();
state.SpendChfSample = enriched.Sum(row => row.NetValue);
state.TopSupplierLabel = BuildTopLabel(enriched.GroupBy(row => row.Supplier), row => row.NetValue, "Lieferant");
state.TopMaterialGroupLabel = BuildTopLabel(enriched.GroupBy(row => row.MaterialGroup), row => row.NetValue, "Warengruppe");
state.TopArticleLabel = BuildTopLabel(enriched.GroupBy(row => row.Material), row => row.NetValue, "Artikel");
state.SpendChartRows = enriched
.GroupBy(row => row.Supplier)
.Select(group => new PurchasingLiveChartPoint($"Lief. {group.Key}", group.Sum(row => row.NetValue)))
.OrderByDescending(row => row.Value)
.Take(6)
.ToList();
}
private static void ApplyEketMetrics(
PurchasingDashboardLiveState state,
List<Dictionary<string, object?>> ekpoRows,
List<Dictionary<string, object?>> eketRows)
{
if (eketRows.Count == 0)
return;
var netPriceByPosition = ekpoRows
.Select(row =>
{
var ebeln = GetText(row, "Ebeln");
var ebelp = GetText(row, "Ebelp");
var key = $"{ebeln}|{ebelp}";
var quantity = GetDecimal(row, "Menge");
var netValue = GetDecimal(row, "Netwr");
var netPrice = quantity == 0 ? 0 : netValue / quantity;
return new { key, netPrice };
})
.GroupBy(row => row.key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.First().netPrice, StringComparer.OrdinalIgnoreCase);
var enriched = eketRows
.Select(row =>
{
var ebeln = GetText(row, "Ebeln");
var ebelp = GetText(row, "Ebelp");
var key = $"{ebeln}|{ebelp}";
netPriceByPosition.TryGetValue(key, out var netPrice);
var quantity = GetDecimal(row, "Menge");
var received = GetDecimal(row, "Wemng");
var openQuantity = Math.Max(0, quantity - received);
return new
{
Ebeln = ebeln,
DueDate = TryParseSapDate(GetText(row, "Eindt")),
OpenQuantity = openQuantity,
OpenValue = openQuantity * netPrice
};
})
.ToList();
state.OpenQuantitySample = enriched.Sum(row => row.OpenQuantity);
state.OpenValueSample = enriched.Sum(row => row.OpenValue);
state.ContractValueSample = state.OpenValueSample;
state.OpenValueChartRows = enriched
.GroupBy(row => row.DueDate?.ToString("yyyy-MM") ?? "ohne Termin")
.Select(group => new PurchasingLiveChartPoint(group.Key, group.Sum(row => row.OpenValue)))
.OrderBy(row => row.Label)
.Take(6)
.ToList();
state.ContractChartRows = state.OpenValueChartRows.ToList();
}
private static HttpClient CreateClient(string username, string password)
{
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(45) };
@@ -151,6 +255,27 @@ public sealed class PurchasingDashboardService : IPurchasingDashboardService
private static string GetText(Dictionary<string, object?> row, string key)
=> row.TryGetValue(key, out var value) ? Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty : string.Empty;
private static decimal GetDecimal(Dictionary<string, object?> row, string key)
{
var text = GetText(row, key);
return decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out var value)
|| decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out value)
? value
: 0m;
}
private static string FirstNonEmpty(params string[] values)
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
private static string BuildTopLabel<T>(IEnumerable<IGrouping<string, T>> groups, Func<T, decimal> selector, string fallback)
{
var top = groups
.Select(group => new { Label = group.Key, Value = group.Sum(selector) })
.OrderByDescending(row => row.Value)
.FirstOrDefault();
return top is null ? fallback : $"{top.Label}: CHF {top.Value:N0}";
}
private static DateTime? TryParseSapDate(string value)
{
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed))