Add product division finance analysis
This commit is contained in:
@@ -284,6 +284,95 @@
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="@T("Sparten-Finanzanalyse", "Division finance")" Icon="@Icons.Material.Filled.PieChart">
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.caption">@T("Gesamtumsatz", "Total sales")</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatValue(_financeResult.ProductFinanceSummary.TotalValue, _financeResult.ProductFinanceSummary.DisplayCurrency)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.caption">@T("Zugeordneter Umsatz", "Assigned sales")</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatValue(_financeResult.ProductFinanceSummary.AssignedValue, _financeResult.ProductFinanceSummary.DisplayCurrency)</MudText>
|
||||
<MudText Typo="Typo.caption">@FormatPercent(_financeResult.ProductFinanceSummary.AssignedValuePercent)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.caption">@T("Nicht zugeordnet", "Unassigned")</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatValue(_financeResult.ProductFinanceSummary.UnassignedValue, _financeResult.ProductFinanceSummary.DisplayCurrency)</MudText>
|
||||
<MudText Typo="Typo.caption">@FormatPercent(_financeResult.ProductFinanceSummary.UnassignedValuePercent)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6" md="3">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.caption">@T("Nicht im Stamm", "Not in master")</MudText>
|
||||
<MudText Typo="Typo.h6">@FormatValue(_financeResult.ProductFinanceSummary.MissingReferenceValue, _financeResult.ProductFinanceSummary.DisplayCurrency)</MudText>
|
||||
<MudText Typo="Typo.caption">@FormatPercent(_financeResult.ProductFinanceSummary.MissingReferenceValuePercent)</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Umsatz nach Produktsparte", "Sales by product division")</MudText>
|
||||
<MudTable Items="_financeResult.ProductDivisionFinanceRows" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Produktsparte", "Product division")</MudTh>
|
||||
<MudTh>@T("Produktfamilie", "Product family")</MudTh>
|
||||
<MudTh>PAPH1</MudTh>
|
||||
<MudTh>@T("Umsatz", "Sales")</MudTh>
|
||||
<MudTh>@T("Anteil", "Share")</MudTh>
|
||||
<MudTh>@T("Materialien", "Materials")</MudTh>
|
||||
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
||||
<MudTh>@T("Laender", "Countries")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText)</MudTd>
|
||||
<MudTd>@BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText)</MudTd>
|
||||
<MudTd>@BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText)</MudTd>
|
||||
<MudTd>@FormatValue(context.NetSalesActual, context.Currency)</MudTd>
|
||||
<MudTd>@FormatPercent(context.SharePercent)</MudTd>
|
||||
<MudTd>@context.MaterialCount.ToString("N0")</MudTd>
|
||||
<MudTd>@context.RowCount.ToString("N0")</MudTd>
|
||||
<MudTd>@context.Countries</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText Typo="Typo.body2">@T("Keine zugeordneten Spartenumsaetze fuer diese Filter.", "No assigned division sales for these filters.")</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Umsatzabdeckung nach Land", "Sales coverage by country")</MudText>
|
||||
<MudTable Items="_financeResult.ProductFinanceCountryRows" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Land", "Country")</MudTh>
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>@T("Gesamt", "Total")</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("Abdeckung", "Coverage")</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.CountryKey</MudTd>
|
||||
<MudTd>@context.Tsc</MudTd>
|
||||
<MudTd>@FormatValue(context.TotalValue, context.Currency)</MudTd>
|
||||
<MudTd>@FormatValue(context.AssignedValue, context.Currency)</MudTd>
|
||||
<MudTd>@FormatValue(context.UnassignedValue, context.Currency)</MudTd>
|
||||
<MudTd>@FormatValue(context.MissingReferenceValue, context.Currency)</MudTd>
|
||||
<MudTd>@FormatValue(context.MissingMaterialValue, context.Currency)</MudTd>
|
||||
<MudTd>@FormatPercent(context.AssignedValuePercent)</MudTd>
|
||||
</RowTemplate>
|
||||
<NoRecordsContent>
|
||||
<MudText Typo="Typo.body2">@T("Keine Umsatzabdeckung fuer diese Filter.", "No sales coverage for these filters.")</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</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">
|
||||
|
||||
@@ -225,6 +225,48 @@ public class ManagementProductAssignmentSummary
|
||||
public int ReferenceMaterialCount { get; set; }
|
||||
}
|
||||
|
||||
public class ManagementProductFinanceSummary
|
||||
{
|
||||
public decimal TotalValue { get; set; }
|
||||
public decimal AssignedValue { get; set; }
|
||||
public decimal UnassignedValue { get; set; }
|
||||
public decimal MissingReferenceValue { get; set; }
|
||||
public decimal MissingMaterialValue { get; set; }
|
||||
public decimal AssignedValuePercent { get; set; }
|
||||
public decimal UnassignedValuePercent { get; set; }
|
||||
public decimal MissingReferenceValuePercent { get; set; }
|
||||
public string DisplayCurrency { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ManagementProductDivisionFinanceRow
|
||||
{
|
||||
public string ProductDivisionCode { get; set; } = string.Empty;
|
||||
public string ProductDivisionText { get; set; } = string.Empty;
|
||||
public string ProductFamilyCode { get; set; } = string.Empty;
|
||||
public string ProductFamilyText { get; set; } = string.Empty;
|
||||
public string ProductHierarchyCode { get; set; } = string.Empty;
|
||||
public string ProductHierarchyText { get; set; } = string.Empty;
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
public decimal NetSalesActual { get; set; }
|
||||
public decimal SharePercent { get; set; }
|
||||
public int MaterialCount { get; set; }
|
||||
public int RowCount { get; set; }
|
||||
public string Countries { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ManagementProductFinanceCountryRow
|
||||
{
|
||||
public string CountryKey { get; set; } = string.Empty;
|
||||
public string Tsc { get; set; } = string.Empty;
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
public decimal TotalValue { get; set; }
|
||||
public decimal AssignedValue { get; set; }
|
||||
public decimal UnassignedValue { get; set; }
|
||||
public decimal MissingReferenceValue { get; set; }
|
||||
public decimal MissingMaterialValue { get; set; }
|
||||
public decimal AssignedValuePercent { get; set; }
|
||||
}
|
||||
|
||||
public class ManagementProductAssignmentCountryRow
|
||||
{
|
||||
public string CountryKey { get; set; } = string.Empty;
|
||||
@@ -278,6 +320,9 @@ public class ManagementFinanceSummaryResult
|
||||
public List<ManagementFinanceDataStatusRow> DataStatusRows { get; set; } = [];
|
||||
public List<ManagementFinanceCreditCandidateRow> CreditCandidates { get; set; } = [];
|
||||
public List<ManagementFinanceDataQualityRow> DataQualityRows { get; set; } = [];
|
||||
public ManagementProductFinanceSummary ProductFinanceSummary { get; set; } = new();
|
||||
public List<ManagementProductDivisionFinanceRow> ProductDivisionFinanceRows { get; set; } = [];
|
||||
public List<ManagementProductFinanceCountryRow> ProductFinanceCountryRows { get; set; } = [];
|
||||
public ManagementProductAssignmentSummary ProductAssignmentSummary { get; set; } = new();
|
||||
public List<ManagementProductAssignmentCountryRow> ProductAssignmentCountryRows { get; set; } = [];
|
||||
public List<ManagementProductAssignmentRow> ProductAssignmentRows { get; set; } = [];
|
||||
|
||||
@@ -460,6 +460,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db);
|
||||
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
|
||||
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
|
||||
var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
|
||||
|
||||
return new ManagementFinanceSummaryResult
|
||||
{
|
||||
@@ -497,6 +498,9 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
DataStatusRows = dataStatusRows,
|
||||
CreditCandidates = BuildFinanceCreditCandidates(scopedRows),
|
||||
DataQualityRows = BuildFinanceDataQualityRows(scopedRows),
|
||||
ProductFinanceSummary = productFinanceSummary,
|
||||
ProductDivisionFinanceRows = BuildProductDivisionFinanceRows(productAssignmentRows),
|
||||
ProductFinanceCountryRows = BuildProductFinanceCountryRows(productAssignmentRows),
|
||||
ProductAssignmentSummary = BuildProductAssignmentSummary(productAssignmentRows),
|
||||
ProductAssignmentCountryRows = BuildProductAssignmentCountryRows(productAssignmentRows),
|
||||
ProductAssignmentRows = productAssignmentRows
|
||||
@@ -711,6 +715,101 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
.Count()
|
||||
};
|
||||
|
||||
private static ManagementProductFinanceSummary BuildProductFinanceSummary(
|
||||
IReadOnlyCollection<ManagementProductAssignmentRow> rows,
|
||||
IReadOnlyCollection<string> currencies)
|
||||
{
|
||||
var total = rows.Sum(row => row.NetSalesActual);
|
||||
var assigned = rows.Where(row => row.Status == ProductAssignmentStatuses.Assigned).Sum(row => row.NetSalesActual);
|
||||
var unassigned = rows.Where(row => row.Status == ProductAssignmentStatuses.Unassigned).Sum(row => row.NetSalesActual);
|
||||
var missingReference = rows.Where(row => row.Status == ProductAssignmentStatuses.NoReference).Sum(row => row.NetSalesActual);
|
||||
var missingMaterial = rows.Where(row => row.Status == ProductAssignmentStatuses.MissingMaterial).Sum(row => row.NetSalesActual);
|
||||
|
||||
return new ManagementProductFinanceSummary
|
||||
{
|
||||
TotalValue = total,
|
||||
AssignedValue = assigned,
|
||||
UnassignedValue = unassigned,
|
||||
MissingReferenceValue = missingReference,
|
||||
MissingMaterialValue = missingMaterial,
|
||||
AssignedValuePercent = PercentOf(assigned, total),
|
||||
UnassignedValuePercent = PercentOf(unassigned, total),
|
||||
MissingReferenceValuePercent = PercentOf(missingReference, total),
|
||||
DisplayCurrency = BuildDisplayCurrencyLabel(currencies)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ManagementProductDivisionFinanceRow> BuildProductDivisionFinanceRows(IEnumerable<ManagementProductAssignmentRow> rows)
|
||||
{
|
||||
var assignedRows = rows
|
||||
.Where(row => row.Status == ProductAssignmentStatuses.Assigned)
|
||||
.ToList();
|
||||
var totalsByCurrency = assignedRows
|
||||
.GroupBy(row => row.Currency, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(group => group.Key, group => group.Sum(row => row.NetSalesActual), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return assignedRows
|
||||
.GroupBy(row => new
|
||||
{
|
||||
row.ProductDivisionCode,
|
||||
row.ProductDivisionText,
|
||||
row.ProductFamilyCode,
|
||||
row.ProductFamilyText,
|
||||
row.ProductHierarchyCode,
|
||||
row.ProductHierarchyText,
|
||||
row.Currency
|
||||
})
|
||||
.Select(group =>
|
||||
{
|
||||
var value = group.Sum(row => row.NetSalesActual);
|
||||
totalsByCurrency.TryGetValue(group.Key.Currency, out var total);
|
||||
return new ManagementProductDivisionFinanceRow
|
||||
{
|
||||
ProductDivisionCode = group.Key.ProductDivisionCode,
|
||||
ProductDivisionText = group.Key.ProductDivisionText,
|
||||
ProductFamilyCode = group.Key.ProductFamilyCode,
|
||||
ProductFamilyText = group.Key.ProductFamilyText,
|
||||
ProductHierarchyCode = group.Key.ProductHierarchyCode,
|
||||
ProductHierarchyText = group.Key.ProductHierarchyText,
|
||||
Currency = group.Key.Currency,
|
||||
NetSalesActual = value,
|
||||
SharePercent = PercentOf(value, total),
|
||||
MaterialCount = group.Select(row => row.Material).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
|
||||
RowCount = group.Sum(row => row.RowCount),
|
||||
Countries = JoinDistinct(group.Select(row => row.CountryKey))
|
||||
};
|
||||
})
|
||||
.OrderByDescending(row => Math.Abs(row.NetSalesActual))
|
||||
.ThenBy(row => row.ProductDivisionCode, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<ManagementProductFinanceCountryRow> BuildProductFinanceCountryRows(IEnumerable<ManagementProductAssignmentRow> rows)
|
||||
=> rows
|
||||
.GroupBy(row => new { row.CountryKey, row.Tsc, row.Currency })
|
||||
.Select(group =>
|
||||
{
|
||||
var rowList = group.ToList();
|
||||
var total = rowList.Sum(row => row.NetSalesActual);
|
||||
var assigned = rowList.Where(row => row.Status == ProductAssignmentStatuses.Assigned).Sum(row => row.NetSalesActual);
|
||||
return new ManagementProductFinanceCountryRow
|
||||
{
|
||||
CountryKey = group.Key.CountryKey,
|
||||
Tsc = group.Key.Tsc,
|
||||
Currency = group.Key.Currency,
|
||||
TotalValue = total,
|
||||
AssignedValue = assigned,
|
||||
UnassignedValue = rowList.Where(row => row.Status == ProductAssignmentStatuses.Unassigned).Sum(row => row.NetSalesActual),
|
||||
MissingReferenceValue = rowList.Where(row => row.Status == ProductAssignmentStatuses.NoReference).Sum(row => row.NetSalesActual),
|
||||
MissingMaterialValue = rowList.Where(row => row.Status == ProductAssignmentStatuses.MissingMaterial).Sum(row => row.NetSalesActual),
|
||||
AssignedValuePercent = PercentOf(assigned, total)
|
||||
};
|
||||
})
|
||||
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(row => row.Tsc, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(row => row.Currency, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
private static List<ManagementProductAssignmentCountryRow> BuildProductAssignmentCountryRows(IEnumerable<ManagementProductAssignmentRow> rows)
|
||||
=> rows
|
||||
.GroupBy(row => new { row.CountryKey, row.Tsc })
|
||||
@@ -774,6 +873,9 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
private static string NormalizeMaterialKey(string value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
|
||||
|
||||
private static decimal PercentOf(decimal value, decimal total)
|
||||
=> total == 0m ? 0m : value * 100m / total;
|
||||
|
||||
private static ManagementFinanceDataQualityRow BuildQualityRow(string issue, int count, int totalRows)
|
||||
{
|
||||
var share = totalRows == 0 ? 0m : count / (decimal)totalRows;
|
||||
|
||||
@@ -368,6 +368,29 @@ public class ManagementCockpitServiceTests : IDisposable
|
||||
row.Tsc == "TRDE" &&
|
||||
row.MatchedMaterialCount == 1 &&
|
||||
row.UnassignedMaterialCount == 1);
|
||||
|
||||
Assert.Equal(260m, result.ProductFinanceSummary.TotalValue);
|
||||
Assert.Equal(180m, result.ProductFinanceSummary.AssignedValue);
|
||||
Assert.Equal(30m, result.ProductFinanceSummary.UnassignedValue);
|
||||
Assert.Equal(50m, result.ProductFinanceSummary.MissingReferenceValue);
|
||||
Assert.Equal(180m * 100m / 260m, result.ProductFinanceSummary.AssignedValuePercent);
|
||||
|
||||
Assert.Contains(result.ProductDivisionFinanceRows, row =>
|
||||
row.ProductDivisionCode == "0001" &&
|
||||
row.Currency == "EUR" &&
|
||||
row.NetSalesActual == 80m &&
|
||||
row.MaterialCount == 1 &&
|
||||
row.Countries == "DE");
|
||||
Assert.Contains(result.ProductDivisionFinanceRows, row =>
|
||||
row.ProductDivisionCode == "0001" &&
|
||||
row.Currency == "CHF" &&
|
||||
row.NetSalesActual == 100m);
|
||||
|
||||
var deFinanceCoverage = Assert.Single(result.ProductFinanceCountryRows, row => row.CountryKey == "DE" && row.Tsc == "TRDE");
|
||||
Assert.Equal(100m, deFinanceCoverage.TotalValue);
|
||||
Assert.Equal(80m, deFinanceCoverage.AssignedValue);
|
||||
Assert.Equal(20m, deFinanceCoverage.UnassignedValue);
|
||||
Assert.Equal(80m, deFinanceCoverage.AssignedValuePercent);
|
||||
}
|
||||
|
||||
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
|
||||
|
||||
Reference in New Issue
Block a user