Load purchasing EKPO and EKET metrics
This commit is contained in:
@@ -16,5 +16,17 @@ public sealed class PurchasingDashboardLiveState
|
||||
public DateTime? LatestOrderDate { get; set; }
|
||||
public int PositionSampleCount { get; set; }
|
||||
public int ScheduleSampleCount { get; set; }
|
||||
public decimal SpendChfSample { get; set; }
|
||||
public decimal OpenQuantitySample { get; set; }
|
||||
public decimal OpenValueSample { get; set; }
|
||||
public decimal ContractValueSample { get; set; }
|
||||
public string TopSupplierLabel { get; set; } = string.Empty;
|
||||
public string TopMaterialGroupLabel { get; set; } = string.Empty;
|
||||
public string TopArticleLabel { get; set; } = string.Empty;
|
||||
public List<PurchasingLiveChartPoint> SpendChartRows { get; set; } = [];
|
||||
public List<PurchasingLiveChartPoint> OpenValueChartRows { get; set; } = [];
|
||||
public List<PurchasingLiveChartPoint> ContractChartRows { get; set; } = [];
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record PurchasingLiveChartPoint(string Label, decimal Value);
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user