From 8cb5f98562264c3c064637aa51f8909e37d1e050 Mon Sep 17 00:00:00 2001 From: metacube Date: Fri, 29 May 2026 08:54:30 +0200 Subject: [PATCH] Add central product assignment tab --- .../Components/Pages/ManagementCockpit.razor | 126 +++++++++++++ .../Models/ManagementCockpitModels.cs | 46 +++++ .../Services/ManagementCockpitService.cs | 175 +++++++++++++++++- .../ManagementCockpitServiceTests.cs | 79 +++++++- .../PRODUCT_SPARTEN_MAPPING_2026-05-27.md | 84 +++++++++ .../docs/rag/PRODUCT_MAPPING.md | 16 ++ TrafagSalesExporter/lastchange.md | 29 +++ 7 files changed, 551 insertions(+), 4 deletions(-) diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor index 7d6a3ff..e2c6320 100644 --- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor +++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor @@ -284,6 +284,115 @@ + + + + + @T("Materialien", "Materials") + @_financeResult.ProductAssignmentSummary.DistinctMaterialCount.ToString("N0") + + + + + @T("Zugeordnet", "Assigned") + @_financeResult.ProductAssignmentSummary.MatchedMaterialCount.ToString("N0") + + + + + @T("Nicht zugeordnet", "Unassigned") + @_financeResult.ProductAssignmentSummary.UnassignedMaterialCount.ToString("N0") + + + + + @T("Nicht im Stamm", "Not in master") + @_financeResult.ProductAssignmentSummary.MissingReferenceMaterialCount.ToString("N0") + + + + + @T("Material fehlt", "Material missing") + @_financeResult.ProductAssignmentSummary.MissingMaterialNumberCount.ToString("N0") + + + + + @T("TR-AG Referenz", "TR AG reference") + @_financeResult.ProductAssignmentSummary.ReferenceMaterialCount.ToString("N0") + + + + + + @T("Diese Sicht prueft Materialnummern aller gefilterten Laender gegen die fuehrende TR-AG-Referenz aus `ProductDivisionRefSet`. Die Produktsparten der lokalen ERPs werden nicht verwendet.", + "This view checks material numbers from all filtered countries against the leading TR AG reference from `ProductDivisionRefSet`. Local ERP product divisions are not used.") + + + + @T("Abdeckung nach Land", "Coverage by country") + + + @T("Land", "Country") + TSC + @T("Materialien", "Materials") + @T("Zugeordnet", "Assigned") + @T("Nicht zugeordnet", "Unassigned") + @T("Nicht im Stamm", "Not in master") + @T("Material fehlt", "Material missing") + @T("Trefferquote", "Match rate") + + + @context.CountryKey + @context.Tsc + @context.DistinctMaterialCount.ToString("N0") + @context.MatchedMaterialCount.ToString("N0") + @context.UnassignedMaterialCount.ToString("N0") + @context.MissingReferenceMaterialCount.ToString("N0") + @context.MissingMaterialNumberCount.ToString("N0") + @FormatPercent(context.MatchPercent) + + + @T("Keine Materialdaten fuer diese Filter.", "No material data for these filters.") + + + + + + @T("Materialpruefung gegen TR-AG-Referenz", "Material check against TR AG reference") + + + @T("Status", "Status") + @T("Land", "Country") + TSC + @T("Land-Material", "Local material") + @T("Land-Text", "Local text") + @T("TR-AG-MATNR", "TR AG MATNR") + PAPH1 + @T("Produktfamilie", "Product family") + @T("Produktsparte", "Product division") + @T("Zeilen", "Rows") + @T("Finance-Wert", "Finance value") + + + @context.Status + @context.CountryKey + @context.Tsc + @context.Material + @context.ArticleName + @context.ReferenceMaterial + @BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText) + @BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText) + @BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText) + @context.RowCount.ToString("N0") + @FormatValue(context.NetSalesActual, context.Currency) + + + @T("Keine Materialpruefung fuer diese Filter.", "No material check for these filters.") + + + + @@ -827,6 +936,23 @@ _ => Color.Info }; + private static Color ProductAssignmentColor(string status) => status switch + { + "Zugeordnet" => Color.Success, + "Nicht zugeordnet" => Color.Warning, + "Nicht im TR-AG-Stamm" => Color.Error, + "Material fehlt" => Color.Default, + _ => Color.Info + }; + + private static string BuildCodeText(string code, string text) + { + if (string.IsNullOrWhiteSpace(code)) + return string.IsNullOrWhiteSpace(text) ? "-" : text; + + return string.IsNullOrWhiteSpace(text) ? code : $"{code} - {text}"; + } + private void SetSelectedCentralAdditionalValueFields(IEnumerable values) { _selectedCentralAdditionalValueFields = values diff --git a/TrafagSalesExporter/Models/ManagementCockpitModels.cs b/TrafagSalesExporter/Models/ManagementCockpitModels.cs index 3fea0c5..90bc973 100644 --- a/TrafagSalesExporter/Models/ManagementCockpitModels.cs +++ b/TrafagSalesExporter/Models/ManagementCockpitModels.cs @@ -215,6 +215,49 @@ public class ManagementFinanceDataQualityRow public string Severity { get; set; } = "Info"; } +public class ManagementProductAssignmentSummary +{ + public int DistinctMaterialCount { get; set; } + public int MatchedMaterialCount { get; set; } + public int UnassignedMaterialCount { get; set; } + public int MissingReferenceMaterialCount { get; set; } + public int MissingMaterialNumberCount { get; set; } + public int ReferenceMaterialCount { get; set; } +} + +public class ManagementProductAssignmentCountryRow +{ + public string CountryKey { get; set; } = string.Empty; + public string Tsc { get; set; } = string.Empty; + public int DistinctMaterialCount { get; set; } + public int MatchedMaterialCount { get; set; } + public int UnassignedMaterialCount { get; set; } + public int MissingReferenceMaterialCount { get; set; } + public int MissingMaterialNumberCount { get; set; } + public decimal MatchPercent { get; set; } +} + +public class ManagementProductAssignmentRow +{ + public string Status { get; set; } = string.Empty; + public string CountryKey { get; set; } = string.Empty; + public string Tsc { get; set; } = string.Empty; + public string SourceSystem { get; set; } = string.Empty; + public string Material { get; set; } = string.Empty; + public string ArticleName { get; set; } = string.Empty; + public string ReferenceMaterial { get; set; } = string.Empty; + public string ProductHierarchyCode { get; set; } = string.Empty; + public string ProductHierarchyText { get; set; } = string.Empty; + public string ProductFamilyCode { get; set; } = string.Empty; + public string ProductFamilyText { get; set; } = string.Empty; + public string ProductDivisionCode { get; set; } = string.Empty; + public string ProductDivisionText { get; set; } = string.Empty; + public string ProductMappingAssigned { get; set; } = string.Empty; + public int RowCount { get; set; } + public decimal NetSalesActual { get; set; } + public string Currency { get; set; } = string.Empty; +} + public class ManagementFinanceSummaryResult { public ManagementFinanceSummaryFilter Filter { get; set; } = new(); @@ -235,4 +278,7 @@ public class ManagementFinanceSummaryResult public List DataStatusRows { get; set; } = []; public List CreditCandidates { get; set; } = []; public List DataQualityRows { get; set; } = []; + public ManagementProductAssignmentSummary ProductAssignmentSummary { get; set; } = new(); + public List ProductAssignmentCountryRows { get; set; } = []; + public List ProductAssignmentRows { get; set; } = []; } diff --git a/TrafagSalesExporter/Services/ManagementCockpitService.cs b/TrafagSalesExporter/Services/ManagementCockpitService.cs index 3871b7a..e9f8076 100644 --- a/TrafagSalesExporter/Services/ManagementCockpitService.cs +++ b/TrafagSalesExporter/Services/ManagementCockpitService.cs @@ -53,6 +53,14 @@ public class ManagementCockpitService : IManagementCockpitService } ]; + private static class ProductAssignmentStatuses + { + public const string Assigned = "Zugeordnet"; + public const string Unassigned = "Nicht zugeordnet"; + public const string NoReference = "Nicht im TR-AG-Stamm"; + public const string MissingMaterial = "Material fehlt"; + } + public async Task> GetAvailableFilesAsync() { using var db = await _dbFactory.CreateDbContextAsync(); @@ -322,6 +330,13 @@ public class ManagementCockpitService : IManagementCockpitService Material = r.Material, Name = r.Name, ProductGroup = r.ProductGroup, + ProductHierarchyCode = r.ProductHierarchyCode, + ProductHierarchyText = r.ProductHierarchyText, + ProductFamilyCode = r.ProductFamilyCode, + ProductFamilyText = r.ProductFamilyText, + ProductDivisionCode = r.ProductDivisionCode, + ProductDivisionText = r.ProductDivisionText, + ProductMappingAssigned = r.ProductMappingAssigned, Quantity = r.Quantity, SupplierCountry = r.SupplierCountry, CustomerNumber = r.CustomerNumber, @@ -363,7 +378,15 @@ public class ManagementCockpitService : IManagementCockpitService InvoiceNumber = record.InvoiceNumber, DocumentType = record.DocumentType, Material = record.Material, + ArticleName = record.Name, ProductGroup = record.ProductGroup, + ProductHierarchyCode = record.ProductHierarchyCode, + ProductHierarchyText = record.ProductHierarchyText, + ProductFamilyCode = record.ProductFamilyCode, + ProductFamilyText = record.ProductFamilyText, + ProductDivisionCode = record.ProductDivisionCode, + ProductDivisionText = record.ProductDivisionText, + ProductMappingAssigned = record.ProductMappingAssigned, CustomerName = record.CustomerName, PostingDate = record.PostingDate, InvoiceDate = record.InvoiceDate, @@ -436,6 +459,7 @@ public class ManagementCockpitService : IManagementCockpitService var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db); var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey); + var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows); return new ManagementFinanceSummaryResult { @@ -472,7 +496,10 @@ public class ManagementCockpitService : IManagementCockpitService .ToList(), DataStatusRows = dataStatusRows, CreditCandidates = BuildFinanceCreditCandidates(scopedRows), - DataQualityRows = BuildFinanceDataQualityRows(scopedRows) + DataQualityRows = BuildFinanceDataQualityRows(scopedRows), + ProductAssignmentSummary = BuildProductAssignmentSummary(productAssignmentRows), + ProductAssignmentCountryRows = BuildProductAssignmentCountryRows(productAssignmentRows), + ProductAssignmentRows = productAssignmentRows }; } @@ -609,6 +636,144 @@ public class ManagementCockpitService : IManagementCockpitService .ToList(); } + private static List BuildProductAssignmentRows( + IReadOnlyCollection scopedRows, + IReadOnlyCollection allRows) + { + var referenceByMaterial = allRows + .Where(row => !string.IsNullOrWhiteSpace(row.Material)) + .Where(row => HasProductReference(row)) + .GroupBy(row => NormalizeMaterialKey(row.Material), StringComparer.OrdinalIgnoreCase) + .ToDictionary( + group => group.Key, + group => group + .OrderByDescending(row => IsAssignedProductReference(row)) + .ThenBy(row => row.Tsc, StringComparer.OrdinalIgnoreCase) + .First(), + StringComparer.OrdinalIgnoreCase); + + return scopedRows + .GroupBy(row => new + { + MaterialKey = NormalizeMaterialKey(row.Material), + row.Material, + row.ArticleName, + row.CountryKey, + row.Tsc, + row.SourceSystem, + row.Currency + }) + .Select(group => + { + var material = group.Key.Material?.Trim() ?? string.Empty; + referenceByMaterial.TryGetValue(group.Key.MaterialKey, out var reference); + var status = BuildProductAssignmentStatus(material, reference); + return new ManagementProductAssignmentRow + { + Status = status, + CountryKey = group.Key.CountryKey, + Tsc = group.Key.Tsc, + SourceSystem = group.Key.SourceSystem, + Material = material, + ArticleName = group.Key.ArticleName, + ReferenceMaterial = reference?.Material ?? string.Empty, + ProductHierarchyCode = reference?.ProductHierarchyCode ?? string.Empty, + ProductHierarchyText = reference?.ProductHierarchyText ?? string.Empty, + ProductFamilyCode = reference?.ProductFamilyCode ?? string.Empty, + ProductFamilyText = reference?.ProductFamilyText ?? string.Empty, + ProductDivisionCode = reference?.ProductDivisionCode ?? string.Empty, + ProductDivisionText = reference?.ProductDivisionText ?? string.Empty, + ProductMappingAssigned = reference?.ProductMappingAssigned ?? string.Empty, + RowCount = group.Count(), + NetSalesActual = group.Sum(row => row.Value), + Currency = group.Key.Currency + }; + }) + .OrderBy(row => ProductAssignmentStatusSort(row.Status)) + .ThenBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase) + .ThenByDescending(row => Math.Abs(row.NetSalesActual)) + .ThenBy(row => row.Material, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static ManagementProductAssignmentSummary BuildProductAssignmentSummary(IReadOnlyCollection rows) + => new() + { + DistinctMaterialCount = rows.Count, + MatchedMaterialCount = rows.Count(row => row.Status == ProductAssignmentStatuses.Assigned), + UnassignedMaterialCount = rows.Count(row => row.Status == ProductAssignmentStatuses.Unassigned), + MissingReferenceMaterialCount = rows.Count(row => row.Status == ProductAssignmentStatuses.NoReference), + MissingMaterialNumberCount = rows.Count(row => row.Status == ProductAssignmentStatuses.MissingMaterial), + ReferenceMaterialCount = rows + .Where(row => !string.IsNullOrWhiteSpace(row.ReferenceMaterial)) + .Select(row => row.ReferenceMaterial) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count() + }; + + private static List BuildProductAssignmentCountryRows(IEnumerable rows) + => rows + .GroupBy(row => new { row.CountryKey, row.Tsc }) + .OrderBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase) + .ThenBy(group => group.Key.Tsc, StringComparer.OrdinalIgnoreCase) + .Select(group => + { + var rowList = group.ToList(); + var matched = rowList.Count(row => row.Status == ProductAssignmentStatuses.Assigned); + var relevant = rowList.Count(row => row.Status != ProductAssignmentStatuses.MissingMaterial); + return new ManagementProductAssignmentCountryRow + { + CountryKey = group.Key.CountryKey, + Tsc = group.Key.Tsc, + DistinctMaterialCount = rowList.Count, + MatchedMaterialCount = matched, + UnassignedMaterialCount = rowList.Count(row => row.Status == ProductAssignmentStatuses.Unassigned), + MissingReferenceMaterialCount = rowList.Count(row => row.Status == ProductAssignmentStatuses.NoReference), + MissingMaterialNumberCount = rowList.Count(row => row.Status == ProductAssignmentStatuses.MissingMaterial), + MatchPercent = relevant == 0 ? 0m : matched * 100m / relevant + }; + }) + .ToList(); + + private static string BuildProductAssignmentStatus(string material, FinanceAggregationRow? reference) + { + if (string.IsNullOrWhiteSpace(material)) + return ProductAssignmentStatuses.MissingMaterial; + if (reference is null) + return ProductAssignmentStatuses.NoReference; + return IsAssignedProductReference(reference) + ? ProductAssignmentStatuses.Assigned + : ProductAssignmentStatuses.Unassigned; + } + + private static bool HasProductReference(FinanceAggregationRow row) + => !string.IsNullOrWhiteSpace(row.ProductHierarchyCode) || + !string.IsNullOrWhiteSpace(row.ProductFamilyCode) || + !string.IsNullOrWhiteSpace(row.ProductDivisionCode) || + !string.IsNullOrWhiteSpace(row.ProductMappingAssigned); + + private static bool IsAssignedProductReference(FinanceAggregationRow row) + => IsTruthy(row.ProductMappingAssigned) && + !string.IsNullOrWhiteSpace(row.ProductDivisionCode) && + !string.Equals(row.ProductDivisionCode, "UNASS", StringComparison.OrdinalIgnoreCase); + + private static bool IsTruthy(string value) + => value.Equals("X", StringComparison.OrdinalIgnoreCase) || + value.Equals("TRUE", StringComparison.OrdinalIgnoreCase) || + value.Equals("1", StringComparison.OrdinalIgnoreCase) || + value.Equals("JA", StringComparison.OrdinalIgnoreCase); + + private static int ProductAssignmentStatusSort(string status) => status switch + { + ProductAssignmentStatuses.NoReference => 0, + ProductAssignmentStatuses.Unassigned => 1, + ProductAssignmentStatuses.MissingMaterial => 2, + _ => 3 + }; + + private static string NormalizeMaterialKey(string value) + => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant(); + private static ManagementFinanceDataQualityRow BuildQualityRow(string issue, int count, int totalRows) { var share = totalRows == 0 ? 0m : count / (decimal)totalRows; @@ -1325,7 +1490,15 @@ public class ManagementCockpitService : IManagementCockpitService public string InvoiceNumber { get; set; } = string.Empty; public string DocumentType { get; set; } = string.Empty; public string Material { get; set; } = string.Empty; + public string ArticleName { get; set; } = string.Empty; public string ProductGroup { get; set; } = string.Empty; + public string ProductHierarchyCode { get; set; } = string.Empty; + public string ProductHierarchyText { get; set; } = string.Empty; + public string ProductFamilyCode { get; set; } = string.Empty; + public string ProductFamilyText { get; set; } = string.Empty; + public string ProductDivisionCode { get; set; } = string.Empty; + public string ProductDivisionText { get; set; } = string.Empty; + public string ProductMappingAssigned { get; set; } = string.Empty; public string CustomerName { get; set; } = string.Empty; public DateTime? PostingDate { get; set; } public DateTime? InvoiceDate { get; set; } diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs index 7ccc7ea..9da2858 100644 --- a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs @@ -313,6 +313,63 @@ public class ManagementCockpitServiceTests : IDisposable Assert.Contains(result.DataQualityRows, row => row.Issue == "Nullwerte im Finance-Wert" && row.Count == 1); } + [Fact] + public async Task AnalyzeFinanceSummaryAsync_Builds_Central_Product_Assignment_Tab_Data() + { + await SeedCentralRowsAsync( + CreateRow("SAP", "Schweiz", "ZSCHWEIZ", "CH-1", "CHF", 100m, new DateTime(2025, 1, 10), + material: "MAT-OK", + name: "Reference article", + productHierarchyCode: "0414", + productHierarchyText: "Industat innen", + productFamilyCode: "0004", + productFamilyText: "Industat", + productDivisionCode: "0001", + productDivisionText: "Thermostate", + productMappingAssigned: "X"), + CreateRow("SAP", "Schweiz", "ZSCHWEIZ", "CH-2", "CHF", 10m, new DateTime(2025, 1, 10), + material: "MAT-UNASS", + productHierarchyCode: "0509", + productHierarchyText: "Multistat", + productDivisionCode: "UNASS", + productDivisionText: "Nicht zugeordnet", + productMappingAssigned: "false"), + CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "DE-1", "EUR", 80m, new DateTime(2025, 1, 11), + material: "MAT-OK", + name: "German article"), + CreateRow("MANUAL_EXCEL", "Italien", "TRIT", "IT-1", "EUR", 50m, new DateTime(2025, 1, 12), + material: "MAT-MISSING", + name: "Unknown article"), + CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "DE-2", "EUR", 20m, new DateTime(2025, 1, 13), + material: "MAT-UNASS", + name: "Unassigned article")); + + var result = await _service.AnalyzeFinanceSummaryAsync(2025, null, null); + + Assert.Equal(5, result.ProductAssignmentSummary.DistinctMaterialCount); + Assert.Equal(2, result.ProductAssignmentSummary.MatchedMaterialCount); + Assert.Equal(2, result.ProductAssignmentSummary.UnassignedMaterialCount); + Assert.Equal(1, result.ProductAssignmentSummary.MissingReferenceMaterialCount); + + var assigned = Assert.Single(result.ProductAssignmentRows, row => row.Material == "MAT-OK" && row.Tsc == "TRDE"); + Assert.Equal("Zugeordnet", assigned.Status); + Assert.Equal("0414", assigned.ProductHierarchyCode); + Assert.Equal("0001", assigned.ProductDivisionCode); + + var missing = Assert.Single(result.ProductAssignmentRows, row => row.Material == "MAT-MISSING" && row.Tsc == "TRIT"); + Assert.Equal("Nicht im TR-AG-Stamm", missing.Status); + + var unassigned = Assert.Single(result.ProductAssignmentRows, row => row.Material == "MAT-UNASS" && row.Tsc == "TRDE"); + Assert.Equal("Nicht zugeordnet", unassigned.Status); + Assert.Equal("UNASS", unassigned.ProductDivisionCode); + + Assert.Contains(result.ProductAssignmentCountryRows, row => + row.CountryKey == "DE" && + row.Tsc == "TRDE" && + row.MatchedMaterialCount == 1 && + row.UnassignedMaterialCount == 1); + } + private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows) { await using var db = await _dbFactory.CreateDbContextAsync(); @@ -351,7 +408,16 @@ public class ManagementCockpitServiceTests : IDisposable DateTime? invoiceDate, DateTime? extractionDate = null, decimal quantity = 1m, - decimal standardCost = 1m) + decimal standardCost = 1m, + string material = "MAT", + string name = "Article", + string productHierarchyCode = "", + string productHierarchyText = "", + string productFamilyCode = "", + string productFamilyText = "", + string productDivisionCode = "", + string productDivisionText = "", + string productMappingAssigned = "") { return new CentralSalesRecord { @@ -362,9 +428,16 @@ public class ManagementCockpitServiceTests : IDisposable Tsc = tsc, InvoiceNumber = invoiceNumber, PositionOnInvoice = 1, - Material = "MAT", - Name = "Article", + Material = material, + Name = name, ProductGroup = "PG", + ProductHierarchyCode = productHierarchyCode, + ProductHierarchyText = productHierarchyText, + ProductFamilyCode = productFamilyCode, + ProductFamilyText = productFamilyText, + ProductDivisionCode = productDivisionCode, + ProductDivisionText = productDivisionText, + ProductMappingAssigned = productMappingAssigned, Quantity = quantity, SupplierNumber = "SUP", SupplierName = "Supplier", diff --git a/TrafagSalesExporter/docs/PRODUCT_SPARTEN_MAPPING_2026-05-27.md b/TrafagSalesExporter/docs/PRODUCT_SPARTEN_MAPPING_2026-05-27.md index 43f56c9..9efa5b1 100644 --- a/TrafagSalesExporter/docs/PRODUCT_SPARTEN_MAPPING_2026-05-27.md +++ b/TrafagSalesExporter/docs/PRODUCT_SPARTEN_MAPPING_2026-05-27.md @@ -275,3 +275,87 @@ Naechster fachlicher/technischer Schritt: - Stimmen Join-Treffer fuer bekannte Materialien? - Wie viele Zeilen bleiben `UNASS` / `Nicht zugeordnet`? - SAP-seitig muss `FINANZDATASCHWEI_GET_ENTITYSET` auf den alten `ZSCHWEIZ`-Select-Code zurueckgesetzt sein, falls er versehentlich mit Produktsparten-Code ueberschrieben wurde. + +## Nachtrag 2026-05-29 Zentrale Spartenzuordnung + +Fachliches Ziel aus Finance-Input: + +- Die Produktsparten-/Produktfamilienzuordnung der anderen Laender-ERPs ist nicht fuehrend. +- Fuehrend ist die Trafag-AG-/SAP-Referenz aus dem eigenen SAP-System. +- Jede Umsatzzeile aus `CentralSalesRecords` wird ueber ihre Materialnummer gegen die TR-AG-Referenz geprueft. +- Wenn die Materialnummer im TR-AG-Stamm vorhanden ist, wird die dortige Produktzuordnung angezeigt. +- Wenn die Materialnummer nicht im TR-AG-Stamm vorhanden ist, gilt der Status `Nicht im TR-AG-Stamm`. +- Wenn die Materialnummer im TR-AG-Stamm vorhanden ist, aber dort `UNASS`/nicht zugeordnet ist, gilt der Status `Nicht zugeordnet`. + +Umsetzung im Web: + +- Neuer Reiter in `Management Analyse`: + - `Zentrale Spartenzuordnung` +- Der Reiter arbeitet auf dem bestehenden Finance-Filter: + - Jahr + - Land + - Waehrung +- Die Referenz wird aus zentral gespeicherten Zeilen mit Produktfeldern gebildet. +- Der Abgleich erfolgt ueber normalisierte Materialnummer: + - Land-ERP-Material links + - TR-AG-Referenz-Material plus Produktzuordnung rechts +- Angezeigte Statuswerte: + - `Zugeordnet` + - `Nicht zugeordnet` + - `Nicht im TR-AG-Stamm` + - `Material fehlt` + +UI-Inhalte: + +- Kennzahlen: + - Materialien + - Zugeordnet + - Nicht zugeordnet + - Nicht im Stamm + - Material fehlt + - TR-AG Referenz +- Laenderuebersicht: + - Land + - TSC + - Materialanzahl + - Zugeordnet + - Nicht zugeordnet + - Nicht im Stamm + - Material fehlt + - Trefferquote +- Detailtabelle: + - Status + - Land + - TSC + - Land-Material + - Land-Text + - TR-AG-MATNR + - PAPH1 + - Produktfamilie + - Produktsparte + - Zeilen + - Finance-Wert + +Technische Dateien: + +- `Models/ManagementCockpitModels.cs` + - neue Modelle fuer Produktzuordnungs-Summary, Laenderzeilen und Detailzeilen. +- `Services/ManagementCockpitService.cs` + - baut die TR-AG-Referenz aus Produktfeldern. + - prueft gefilterte Finance-Zeilen ueber `Material`. + - erzeugt Summary, Laenderabdeckung und Detailzeilen. +- `Components/Pages/ManagementCockpit.razor` + - neuer Reiter `Zentrale Spartenzuordnung`. +- `TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs` + - Test fuer Treffer, fehlende Referenz und `UNASS`. + +Validierung: + +- `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-central-product-assignment` +- Ergebnis: `80/80` Tests gruen. + +Wichtig: + +- Die Sicht ist zunaechst eine Pruef-/Analyseansicht. +- Sie veraendert noch keine bestehenden Umsatzzeilen der anderen Laender. +- Persistente Anreicherung aller `CentralSalesRecords` kann spaeter folgen, wenn die Treffer-/Fehlerquote fachlich akzeptiert ist. diff --git a/TrafagSalesExporter/docs/rag/PRODUCT_MAPPING.md b/TrafagSalesExporter/docs/rag/PRODUCT_MAPPING.md index 33ef963..8f4c1f1 100644 --- a/TrafagSalesExporter/docs/rag/PRODUCT_MAPPING.md +++ b/TrafagSalesExporter/docs/rag/PRODUCT_MAPPING.md @@ -60,6 +60,21 @@ Stand: 2026-05-29 - Lokale App wurde neu gestartet; `http://localhost:55416/` antwortet mit HTTP 200. - Validierung: `79/79` Tests gruen mit separatem Artefaktpfad. +## Zentrale Spartenzuordnung + +- Neuer Reiter in `Management Analyse`: `Zentrale Spartenzuordnung`. +- Zweck: Materialnummern aller Laender gegen die fuehrende TR-AG-/SAP-Referenz pruefen. +- Lokale ERP-Produktzuordnungen anderer Laender sind nicht fuehrend. +- Statuslogik: + - Treffer mit zugeordneter TR-AG-Sparte: `Zugeordnet`. + - Treffer mit `UNASS`/nicht zugeordnet: `Nicht zugeordnet`. + - Kein Treffer im TR-AG-Stamm: `Nicht im TR-AG-Stamm`. + - Leere Materialnummer: `Material fehlt`. +- Die Sicht nutzt den bestehenden Finance-Filter fuer Jahr/Land/Waehrung. +- Sie zeigt Kennzahlen, Laenderabdeckung und Detailzeilen mit Land-Material links und TR-AG-Referenz rechts. +- Umsetzung ist eine Analyseansicht, keine persistente Mutation anderer Laenderzeilen. +- Validierung nach Umsetzung: `80/80` Tests gruen. + ## Offene Punkte Fuer Sitzung - Normalisierung der Materialnummern. @@ -71,6 +86,7 @@ Stand: 2026-05-29 - Richtige Texttabellen fuer `WWPFA`/`WWPSP` bestaetigen. - VKORG/VTWEG fuer TR-AG-Referenzlauf bestaetigen. - Standort `ZSCHWEIZ` im Export Dashboard neu laufen lassen und Fuellung der neuen Produktfelder pruefen. +- Treffer-/Fehlerquote im Reiter `Zentrale Spartenzuordnung` pruefen. ## Rohquelle Nur Bei Bedarf diff --git a/TrafagSalesExporter/lastchange.md b/TrafagSalesExporter/lastchange.md index 7754ebd..950ea99 100644 --- a/TrafagSalesExporter/lastchange.md +++ b/TrafagSalesExporter/lastchange.md @@ -61,6 +61,35 @@ Offen: - Danach Fuellung der neuen Produktfelder und Quote `UNASS` pruefen. - Fachliche Mapping-Luecken wie `0509`/`0540` spaeter mit Andreas/Kendra klaeren. +## Nachtrag 2026-05-29 Zentrale Spartenzuordnung + +Umgesetzt: + +- Neuer Reiter in `Management Analyse`: `Zentrale Spartenzuordnung`. +- Fachlogik: + - Andere Laender-ERPs sind fuer Produktsparten nicht fuehrend. + - Fuehrend ist die TR-AG-/SAP-Referenz aus `ProductDivisionRefSet`. + - Umsatzzeilen aus `CentralSalesRecords` werden ueber `Material` gegen die TR-AG-Referenz geprueft. +- Statuswerte: + - `Zugeordnet` + - `Nicht zugeordnet` + - `Nicht im TR-AG-Stamm` + - `Material fehlt` +- Der Reiter zeigt: + - Summary-Kennzahlen + - Abdeckung nach Land/TSC + - Detailtabelle mit Land-Material links und TR-AG-MATNR/PAPH1/Familie/Sparte rechts. +- Die Sicht verwendet die bestehenden Finance-Filter fuer Jahr, Land und Waehrung. +- Noch keine persistente Mutation anderer Laenderzeilen; es ist bewusst eine Pruefansicht. + +Technisch: + +- Neue Modelle in `ManagementCockpitModels`. +- Produktzuordnungsanalyse in `ManagementCockpitService`. +- Neuer Reiter in `Components/Pages/ManagementCockpit.razor`. +- Test ergaenzt: `AnalyzeFinanceSummaryAsync_Builds_Central_Product_Assignment_Tab_Data`. +- Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-central-product-assignment` mit `80/80` Tests gruen. + ## Nachtrag 2026-05-28 ABAP Produktsparten-Mapping Erstellt: