Add central product assignment tab
This commit is contained in:
@@ -284,6 +284,115 @@
|
|||||||
</MudTable>
|
</MudTable>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="@T("Zentrale Spartenzuordnung", "Central division mapping")" Icon="@Icons.Material.Filled.AccountTree">
|
||||||
|
<MudGrid Class="mb-4">
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Materialien", "Materials")</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_financeResult.ProductAssignmentSummary.DistinctMaterialCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Zugeordnet", "Assigned")</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_financeResult.ProductAssignmentSummary.MatchedMaterialCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Nicht zugeordnet", "Unassigned")</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_financeResult.ProductAssignmentSummary.UnassignedMaterialCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Nicht im Stamm", "Not in master")</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_financeResult.ProductAssignmentSummary.MissingReferenceMaterialCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Material fehlt", "Material missing")</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_financeResult.ProductAssignmentSummary.MissingMaterialNumberCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="2">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("TR-AG Referenz", "TR AG reference")</MudText>
|
||||||
|
<MudText Typo="Typo.h6">@_financeResult.ProductAssignmentSummary.ReferenceMaterialCount.ToString("N0")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-4">
|
||||||
|
@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.")
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Abdeckung nach Land", "Coverage by country")</MudText>
|
||||||
|
<MudTable Items="_financeResult.ProductAssignmentCountryRows" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Land", "Country")</MudTh>
|
||||||
|
<MudTh>TSC</MudTh>
|
||||||
|
<MudTh>@T("Materialien", "Materials")</MudTh>
|
||||||
|
<MudTh>@T("Zugeordnet", "Assigned")</MudTh>
|
||||||
|
<MudTh>@T("Nicht zugeordnet", "Unassigned")</MudTh>
|
||||||
|
<MudTh>@T("Nicht im Stamm", "Not in master")</MudTh>
|
||||||
|
<MudTh>@T("Material fehlt", "Material missing")</MudTh>
|
||||||
|
<MudTh>@T("Trefferquote", "Match rate")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.CountryKey</MudTd>
|
||||||
|
<MudTd>@context.Tsc</MudTd>
|
||||||
|
<MudTd>@context.DistinctMaterialCount.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.MatchedMaterialCount.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.UnassignedMaterialCount.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.MissingReferenceMaterialCount.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.MissingMaterialNumberCount.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@FormatPercent(context.MatchPercent)</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.body2">@T("Keine Materialdaten fuer diese Filter.", "No material data for these filters.")</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Materialpruefung gegen TR-AG-Referenz", "Material check against TR AG reference")</MudText>
|
||||||
|
<MudTable Items="_financeResult.ProductAssignmentRows" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Status", "Status")</MudTh>
|
||||||
|
<MudTh>@T("Land", "Country")</MudTh>
|
||||||
|
<MudTh>TSC</MudTh>
|
||||||
|
<MudTh>@T("Land-Material", "Local material")</MudTh>
|
||||||
|
<MudTh>@T("Land-Text", "Local text")</MudTh>
|
||||||
|
<MudTh>@T("TR-AG-MATNR", "TR AG MATNR")</MudTh>
|
||||||
|
<MudTh>PAPH1</MudTh>
|
||||||
|
<MudTh>@T("Produktfamilie", "Product family")</MudTh>
|
||||||
|
<MudTh>@T("Produktsparte", "Product division")</MudTh>
|
||||||
|
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
||||||
|
<MudTh>@T("Finance-Wert", "Finance value")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd><MudChip T="string" Size="Size.Small" Color="@ProductAssignmentColor(context.Status)" Variant="Variant.Outlined">@context.Status</MudChip></MudTd>
|
||||||
|
<MudTd>@context.CountryKey</MudTd>
|
||||||
|
<MudTd>@context.Tsc</MudTd>
|
||||||
|
<MudTd>@context.Material</MudTd>
|
||||||
|
<MudTd>@context.ArticleName</MudTd>
|
||||||
|
<MudTd>@context.ReferenceMaterial</MudTd>
|
||||||
|
<MudTd>@BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText)</MudTd>
|
||||||
|
<MudTd>@BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText)</MudTd>
|
||||||
|
<MudTd>@BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText)</MudTd>
|
||||||
|
<MudTd>@context.RowCount.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@FormatValue(context.NetSalesActual, context.Currency)</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.body2">@T("Keine Materialpruefung fuer diese Filter.", "No material check for these filters.")</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="@T("Rohdaten Diagnose", "Raw-data diagnostics")" Icon="@Icons.Material.Filled.QueryStats">
|
<MudTabPanel Text="@T("Rohdaten Diagnose", "Raw-data diagnostics")" Icon="@Icons.Material.Filled.QueryStats">
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
@@ -827,6 +936,23 @@
|
|||||||
_ => Color.Info
|
_ => 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<string> values)
|
private void SetSelectedCentralAdditionalValueFields(IEnumerable<string> values)
|
||||||
{
|
{
|
||||||
_selectedCentralAdditionalValueFields = values
|
_selectedCentralAdditionalValueFields = values
|
||||||
|
|||||||
@@ -215,6 +215,49 @@ public class ManagementFinanceDataQualityRow
|
|||||||
public string Severity { get; set; } = "Info";
|
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 class ManagementFinanceSummaryResult
|
||||||
{
|
{
|
||||||
public ManagementFinanceSummaryFilter Filter { get; set; } = new();
|
public ManagementFinanceSummaryFilter Filter { get; set; } = new();
|
||||||
@@ -235,4 +278,7 @@ public class ManagementFinanceSummaryResult
|
|||||||
public List<ManagementFinanceDataStatusRow> DataStatusRows { get; set; } = [];
|
public List<ManagementFinanceDataStatusRow> DataStatusRows { get; set; } = [];
|
||||||
public List<ManagementFinanceCreditCandidateRow> CreditCandidates { get; set; } = [];
|
public List<ManagementFinanceCreditCandidateRow> CreditCandidates { get; set; } = [];
|
||||||
public List<ManagementFinanceDataQualityRow> DataQualityRows { get; set; } = [];
|
public List<ManagementFinanceDataQualityRow> DataQualityRows { get; set; } = [];
|
||||||
|
public ManagementProductAssignmentSummary ProductAssignmentSummary { get; set; } = new();
|
||||||
|
public List<ManagementProductAssignmentCountryRow> ProductAssignmentCountryRows { get; set; } = [];
|
||||||
|
public List<ManagementProductAssignmentRow> ProductAssignmentRows { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<List<ManagementCockpitFileOption>> GetAvailableFilesAsync()
|
public async Task<List<ManagementCockpitFileOption>> GetAvailableFilesAsync()
|
||||||
{
|
{
|
||||||
using var db = await _dbFactory.CreateDbContextAsync();
|
using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -322,6 +330,13 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
Material = r.Material,
|
Material = r.Material,
|
||||||
Name = r.Name,
|
Name = r.Name,
|
||||||
ProductGroup = r.ProductGroup,
|
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,
|
Quantity = r.Quantity,
|
||||||
SupplierCountry = r.SupplierCountry,
|
SupplierCountry = r.SupplierCountry,
|
||||||
CustomerNumber = r.CustomerNumber,
|
CustomerNumber = r.CustomerNumber,
|
||||||
@@ -363,7 +378,15 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
InvoiceNumber = record.InvoiceNumber,
|
InvoiceNumber = record.InvoiceNumber,
|
||||||
DocumentType = record.DocumentType,
|
DocumentType = record.DocumentType,
|
||||||
Material = record.Material,
|
Material = record.Material,
|
||||||
|
ArticleName = record.Name,
|
||||||
ProductGroup = record.ProductGroup,
|
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,
|
CustomerName = record.CustomerName,
|
||||||
PostingDate = record.PostingDate,
|
PostingDate = record.PostingDate,
|
||||||
InvoiceDate = record.InvoiceDate,
|
InvoiceDate = record.InvoiceDate,
|
||||||
@@ -436,6 +459,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
|
|
||||||
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db);
|
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db);
|
||||||
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
|
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
|
||||||
|
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
|
||||||
|
|
||||||
return new ManagementFinanceSummaryResult
|
return new ManagementFinanceSummaryResult
|
||||||
{
|
{
|
||||||
@@ -472,7 +496,10 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
.ToList(),
|
.ToList(),
|
||||||
DataStatusRows = dataStatusRows,
|
DataStatusRows = dataStatusRows,
|
||||||
CreditCandidates = BuildFinanceCreditCandidates(scopedRows),
|
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();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ManagementProductAssignmentRow> BuildProductAssignmentRows(
|
||||||
|
IReadOnlyCollection<FinanceAggregationRow> scopedRows,
|
||||||
|
IReadOnlyCollection<FinanceAggregationRow> 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<ManagementProductAssignmentRow> 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<ManagementProductAssignmentCountryRow> BuildProductAssignmentCountryRows(IEnumerable<ManagementProductAssignmentRow> 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)
|
private static ManagementFinanceDataQualityRow BuildQualityRow(string issue, int count, int totalRows)
|
||||||
{
|
{
|
||||||
var share = totalRows == 0 ? 0m : count / (decimal)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 InvoiceNumber { get; set; } = string.Empty;
|
||||||
public string DocumentType { get; set; } = string.Empty;
|
public string DocumentType { get; set; } = string.Empty;
|
||||||
public string Material { 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 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 string CustomerName { get; set; } = string.Empty;
|
||||||
public DateTime? PostingDate { get; set; }
|
public DateTime? PostingDate { get; set; }
|
||||||
public DateTime? InvoiceDate { get; set; }
|
public DateTime? InvoiceDate { get; set; }
|
||||||
|
|||||||
@@ -313,6 +313,63 @@ public class ManagementCockpitServiceTests : IDisposable
|
|||||||
Assert.Contains(result.DataQualityRows, row => row.Issue == "Nullwerte im Finance-Wert" && row.Count == 1);
|
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)
|
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
|
||||||
{
|
{
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -351,7 +408,16 @@ public class ManagementCockpitServiceTests : IDisposable
|
|||||||
DateTime? invoiceDate,
|
DateTime? invoiceDate,
|
||||||
DateTime? extractionDate = null,
|
DateTime? extractionDate = null,
|
||||||
decimal quantity = 1m,
|
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
|
return new CentralSalesRecord
|
||||||
{
|
{
|
||||||
@@ -362,9 +428,16 @@ public class ManagementCockpitServiceTests : IDisposable
|
|||||||
Tsc = tsc,
|
Tsc = tsc,
|
||||||
InvoiceNumber = invoiceNumber,
|
InvoiceNumber = invoiceNumber,
|
||||||
PositionOnInvoice = 1,
|
PositionOnInvoice = 1,
|
||||||
Material = "MAT",
|
Material = material,
|
||||||
Name = "Article",
|
Name = name,
|
||||||
ProductGroup = "PG",
|
ProductGroup = "PG",
|
||||||
|
ProductHierarchyCode = productHierarchyCode,
|
||||||
|
ProductHierarchyText = productHierarchyText,
|
||||||
|
ProductFamilyCode = productFamilyCode,
|
||||||
|
ProductFamilyText = productFamilyText,
|
||||||
|
ProductDivisionCode = productDivisionCode,
|
||||||
|
ProductDivisionText = productDivisionText,
|
||||||
|
ProductMappingAssigned = productMappingAssigned,
|
||||||
Quantity = quantity,
|
Quantity = quantity,
|
||||||
SupplierNumber = "SUP",
|
SupplierNumber = "SUP",
|
||||||
SupplierName = "Supplier",
|
SupplierName = "Supplier",
|
||||||
|
|||||||
@@ -275,3 +275,87 @@ Naechster fachlicher/technischer Schritt:
|
|||||||
- Stimmen Join-Treffer fuer bekannte Materialien?
|
- Stimmen Join-Treffer fuer bekannte Materialien?
|
||||||
- Wie viele Zeilen bleiben `UNASS` / `Nicht zugeordnet`?
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -60,6 +60,21 @@ Stand: 2026-05-29
|
|||||||
- Lokale App wurde neu gestartet; `http://localhost:55416/` antwortet mit HTTP 200.
|
- Lokale App wurde neu gestartet; `http://localhost:55416/` antwortet mit HTTP 200.
|
||||||
- Validierung: `79/79` Tests gruen mit separatem Artefaktpfad.
|
- 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
|
## Offene Punkte Fuer Sitzung
|
||||||
|
|
||||||
- Normalisierung der Materialnummern.
|
- Normalisierung der Materialnummern.
|
||||||
@@ -71,6 +86,7 @@ Stand: 2026-05-29
|
|||||||
- Richtige Texttabellen fuer `WWPFA`/`WWPSP` bestaetigen.
|
- Richtige Texttabellen fuer `WWPFA`/`WWPSP` bestaetigen.
|
||||||
- VKORG/VTWEG fuer TR-AG-Referenzlauf bestaetigen.
|
- VKORG/VTWEG fuer TR-AG-Referenzlauf bestaetigen.
|
||||||
- Standort `ZSCHWEIZ` im Export Dashboard neu laufen lassen und Fuellung der neuen Produktfelder pruefen.
|
- 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
|
## Rohquelle Nur Bei Bedarf
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,35 @@ Offen:
|
|||||||
- Danach Fuellung der neuen Produktfelder und Quote `UNASS` pruefen.
|
- Danach Fuellung der neuen Produktfelder und Quote `UNASS` pruefen.
|
||||||
- Fachliche Mapping-Luecken wie `0509`/`0540` spaeter mit Andreas/Kendra klaeren.
|
- 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
|
## Nachtrag 2026-05-28 ABAP Produktsparten-Mapping
|
||||||
|
|
||||||
Erstellt:
|
Erstellt:
|
||||||
|
|||||||
Reference in New Issue
Block a user