Keep finance references in expert analysis
This commit is contained in:
@@ -1446,6 +1446,28 @@
|
|||||||
if (_financeResult is null)
|
if (_financeResult is null)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
|
if (IsFinance3dReferenceYearIndicator(_finance3dIndicator))
|
||||||
|
{
|
||||||
|
return _financeResult.CountryRows
|
||||||
|
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group =>
|
||||||
|
{
|
||||||
|
var rows = group.ToList();
|
||||||
|
var first = rows[0];
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
country = first.CountryKey,
|
||||||
|
year = first.Year,
|
||||||
|
currency = BuildDisplayCurrencyLabel(rows.Select(row => row.Currency).Where(value => value != "-")),
|
||||||
|
value = ResolveFinance3dCountryValue(rows)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.OrderBy(row => row.country, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(row => row.year)
|
||||||
|
.Cast<object>()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
var countryRowsByKey = _financeResult.CountryRows
|
var countryRowsByKey = _financeResult.CountryRows
|
||||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
@@ -1453,9 +1475,7 @@
|
|||||||
group => group.ToList(),
|
group => group.ToList(),
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var sourceRows = IsFinance3dReferenceYearIndicator(_finance3dIndicator)
|
var sourceRows = _financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows;
|
||||||
? _financeResult.Rows
|
|
||||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
|
||||||
|
|
||||||
return sourceRows
|
return sourceRows
|
||||||
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -1481,6 +1501,22 @@
|
|||||||
if (_financeResult is null)
|
if (_financeResult is null)
|
||||||
return 0m;
|
return 0m;
|
||||||
|
|
||||||
|
if (IsFinance3dReferenceYearIndicator(_finance3dIndicator))
|
||||||
|
{
|
||||||
|
var referenceValues = _financeResult.CountryRows
|
||||||
|
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => ResolveFinance3dCountryValue(group.ToList()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (IsFinance3dPercentIndicator(_finance3dIndicator))
|
||||||
|
{
|
||||||
|
var nonZeroValues = referenceValues.Where(value => value != 0m).ToList();
|
||||||
|
return nonZeroValues.Count == 0 ? 0m : nonZeroValues.Average();
|
||||||
|
}
|
||||||
|
|
||||||
|
return referenceValues.Sum();
|
||||||
|
}
|
||||||
|
|
||||||
var countryRowsByKey = _financeResult.CountryRows
|
var countryRowsByKey = _financeResult.CountryRows
|
||||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
@@ -1488,9 +1524,7 @@
|
|||||||
group => group.ToList(),
|
group => group.ToList(),
|
||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var sourceRows = IsFinance3dReferenceYearIndicator(_finance3dIndicator)
|
var sourceRows = _financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows;
|
||||||
? _financeResult.Rows
|
|
||||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
|
||||||
|
|
||||||
var values = sourceRows
|
var values = sourceRows
|
||||||
.Select(row =>
|
.Select(row =>
|
||||||
@@ -1529,6 +1563,15 @@
|
|||||||
_ => Math.Abs(row.NetSalesActual)
|
_ => Math.Abs(row.NetSalesActual)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private decimal ResolveFinance3dCountryValue(IReadOnlyCollection<ManagementFinanceCountryStatusRow> rows)
|
||||||
|
=> _finance3dIndicator switch
|
||||||
|
{
|
||||||
|
Finance3dIndicators.ReferenceValue => Math.Abs(rows.Select(row => row.ReferenceValue).FirstOrDefault(value => value.HasValue) ?? 0m),
|
||||||
|
Finance3dIndicators.Deviation => Math.Abs(rows.Where(row => row.Difference.HasValue).Sum(row => row.Difference!.Value)),
|
||||||
|
Finance3dIndicators.DeviationPercent => Math.Abs(AverageNullablePercent(rows.Select(row => row.DifferencePercent))),
|
||||||
|
_ => 0m
|
||||||
|
};
|
||||||
|
|
||||||
private static bool IsFinance3dReferenceYearIndicator(string indicator)
|
private static bool IsFinance3dReferenceYearIndicator(string indicator)
|
||||||
=> indicator is Finance3dIndicators.ReferenceValue or Finance3dIndicators.Deviation or Finance3dIndicators.DeviationPercent;
|
=> indicator is Finance3dIndicators.ReferenceValue or Finance3dIndicators.Deviation or Finance3dIndicators.DeviationPercent;
|
||||||
|
|
||||||
@@ -1662,6 +1705,17 @@
|
|||||||
? value.ToString("N2")
|
? value.ToString("N2")
|
||||||
: $"{value:N2} {currency}";
|
: $"{value:N2} {currency}";
|
||||||
|
|
||||||
|
private static string BuildDisplayCurrencyLabel(IEnumerable<string> currencies)
|
||||||
|
{
|
||||||
|
var distinct = currencies
|
||||||
|
.Where(currency => !string.IsNullOrWhiteSpace(currency) && currency != "-")
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(currency => currency, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return distinct.Count == 0 ? "-" : string.Join("/", distinct);
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatNullableValue(decimal? value, string currency)
|
private static string FormatNullableValue(decimal? value, string currency)
|
||||||
=> value.HasValue ? FormatValue(value.Value, currency) : "-";
|
=> value.HasValue ? FormatValue(value.Value, currency) : "-";
|
||||||
|
|
||||||
@@ -1714,6 +1768,8 @@
|
|||||||
{
|
{
|
||||||
if (!row.ReferenceValue.HasValue)
|
if (!row.ReferenceValue.HasValue)
|
||||||
return T("Kein Sollwert gepflegt.", "No reference value maintained.");
|
return T("Kein Sollwert gepflegt.", "No reference value maintained.");
|
||||||
|
if (row.TotalRows == 0)
|
||||||
|
return T("Sollwert gepflegt, aber kein Ist im aktuellen Filter.", "Reference maintained, but no actuals in the current filter.");
|
||||||
if (row.Status == "OK")
|
if (row.Status == "OK")
|
||||||
return T("Freigabefaehig.", "Ready for approval.");
|
return T("Freigabefaehig.", "Ready for approval.");
|
||||||
if (row.Difference.HasValue)
|
if (row.Difference.HasValue)
|
||||||
|
|||||||
@@ -390,13 +390,43 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var references = await db.FinanceReferences
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(reference => reference.IsActive)
|
||||||
|
.ToListAsync();
|
||||||
|
var referenceByKey = references
|
||||||
|
.Where(reference => reference.Year == year)
|
||||||
|
.GroupBy(reference => reference.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group => group
|
||||||
|
.Select(reference => new FinanceReferenceValue(
|
||||||
|
reference.Key,
|
||||||
|
reference.Label,
|
||||||
|
reference.CheckValue ?? reference.LocalCurrencyValue))
|
||||||
|
.FirstOrDefault(reference => reference.Value.HasValue),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var yearOptions = allRows
|
var yearOptions = allRows
|
||||||
.Select(row => row.Year)
|
.Select(row => row.Year)
|
||||||
|
.Concat(references.Select(reference => reference.Year))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderBy(yearValue => yearValue)
|
.OrderBy(yearValue => yearValue)
|
||||||
.ToList();
|
.ToList();
|
||||||
if (year == 0)
|
if (year == 0)
|
||||||
year = yearOptions.LastOrDefault();
|
year = yearOptions.LastOrDefault();
|
||||||
|
referenceByKey = references
|
||||||
|
.Where(reference => reference.Year == year)
|
||||||
|
.GroupBy(reference => reference.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group => group
|
||||||
|
.Select(reference => new FinanceReferenceValue(
|
||||||
|
reference.Key,
|
||||||
|
reference.Label,
|
||||||
|
reference.CheckValue ?? reference.LocalCurrencyValue))
|
||||||
|
.FirstOrDefault(reference => reference.Value.HasValue),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var countryFilter = NormalizeOptionalFilter(countryKey);
|
var countryFilter = NormalizeOptionalFilter(countryKey);
|
||||||
var currencyFilter = NormalizeOptionalFilter(currency);
|
var currencyFilter = NormalizeOptionalFilter(currency);
|
||||||
@@ -452,19 +482,8 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
notices.Insert(0, "Fuer die gewaehlten Finance-Filter gibt es keine Datensaetze im aktuellen Zentraldatenbestand.");
|
notices.Insert(0, "Fuer die gewaehlten Finance-Filter gibt es keine Datensaetze im aktuellen Zentraldatenbestand.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var references = await db.FinanceReferences
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(reference => reference.IsActive && reference.Year == year)
|
|
||||||
.ToListAsync();
|
|
||||||
var referenceByKey = references
|
|
||||||
.GroupBy(reference => reference.Key, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(
|
|
||||||
group => group.Key,
|
|
||||||
group => group.Select(reference => reference.CheckValue ?? reference.LocalCurrencyValue).FirstOrDefault(value => value.HasValue),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db, records, settings.UseAuditCsvAsCentralSource);
|
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db, records, settings.UseAuditCsvAsCentralSource);
|
||||||
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
|
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey, year, countryFilter, currencyFilter);
|
||||||
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
|
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
|
||||||
var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
|
var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
|
||||||
notices.AddRange(BuildProductAssignmentNotices(productAssignmentRows, productFinanceSummary));
|
notices.AddRange(BuildProductAssignmentNotices(productAssignmentRows, productFinanceSummary));
|
||||||
@@ -480,6 +499,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
YearOptions = yearOptions,
|
YearOptions = yearOptions,
|
||||||
CountryOptions = allRows
|
CountryOptions = allRows
|
||||||
.Select(row => row.CountryKey)
|
.Select(row => row.CountryKey)
|
||||||
|
.Concat(references.Where(reference => reference.Year == year).Select(reference => reference.Key))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
@@ -647,15 +667,18 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
|
|
||||||
private static List<ManagementFinanceCountryStatusRow> BuildFinanceCountryStatusRows(
|
private static List<ManagementFinanceCountryStatusRow> BuildFinanceCountryStatusRows(
|
||||||
IReadOnlyCollection<FinanceAggregationRow> rows,
|
IReadOnlyCollection<FinanceAggregationRow> rows,
|
||||||
IReadOnlyDictionary<string, decimal?> referenceByKey)
|
IReadOnlyDictionary<string, FinanceReferenceValue?> referenceByKey,
|
||||||
=> rows
|
int year,
|
||||||
|
string? countryFilter,
|
||||||
|
string? currencyFilter)
|
||||||
|
{
|
||||||
|
var actualRows = rows
|
||||||
.GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
|
.GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
|
||||||
.OrderBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(group =>
|
.Select(group =>
|
||||||
{
|
{
|
||||||
var rowList = group.ToList();
|
var rowList = group.ToList();
|
||||||
referenceByKey.TryGetValue(group.Key.CountryKey, out var referenceValue);
|
referenceByKey.TryGetValue(group.Key.CountryKey, out var reference);
|
||||||
|
var referenceValue = reference?.Value;
|
||||||
var actual = rowList.Sum(row => row.Value);
|
var actual = rowList.Sum(row => row.Value);
|
||||||
var intercompanyValue = rowList.Where(row => row.IsIntercompany).Sum(row => row.Value);
|
var intercompanyValue = rowList.Where(row => row.IsIntercompany).Sum(row => row.Value);
|
||||||
var difference = referenceValue.HasValue ? actual - referenceValue.Value : (decimal?)null;
|
var difference = referenceValue.HasValue ? actual - referenceValue.Value : (decimal?)null;
|
||||||
@@ -674,11 +697,38 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
ReferenceValue = referenceValue,
|
ReferenceValue = referenceValue,
|
||||||
Difference = difference,
|
Difference = difference,
|
||||||
DifferencePercent = referenceValue is > 0m && difference.HasValue ? difference.Value / referenceValue.Value * 100m : null,
|
DifferencePercent = referenceValue is > 0m && difference.HasValue ? difference.Value / referenceValue.Value * 100m : null,
|
||||||
Status = BuildFinanceStatus(difference)
|
Status = BuildFinanceStatus(referenceValue, rowList.Count, difference)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var actualCountryKeys = actualRows
|
||||||
|
.Select(row => row.CountryKey)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var referenceOnlyRows = currencyFilter is null
|
||||||
|
? referenceByKey.Values
|
||||||
|
.Where(reference => reference?.Value.HasValue == true)
|
||||||
|
.Select(reference => reference!)
|
||||||
|
.Where(reference => countryFilter is null || reference.Key.Equals(countryFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(reference => !actualCountryKeys.Contains(reference.Key))
|
||||||
|
.Select(reference => new ManagementFinanceCountryStatusRow
|
||||||
|
{
|
||||||
|
Year = year,
|
||||||
|
CountryKey = reference.Key,
|
||||||
|
Currency = "-",
|
||||||
|
ReferenceValue = reference.Value,
|
||||||
|
Status = BuildFinanceStatus(reference.Value, 0, null)
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return actualRows
|
||||||
|
.Concat(referenceOnlyRows)
|
||||||
|
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(row => row.Currency, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static List<ManagementFinanceCreditCandidateRow> BuildFinanceCreditCandidates(IEnumerable<FinanceAggregationRow> rows)
|
private static List<ManagementFinanceCreditCandidateRow> BuildFinanceCreditCandidates(IEnumerable<FinanceAggregationRow> rows)
|
||||||
=> rows
|
=> rows
|
||||||
.Where(row => row.Value < 0m || row.RawSalesValue < 0m || LooksLikeCreditDocument(row.DocumentType, row.InvoiceNumber))
|
.Where(row => row.Value < 0m || row.RawSalesValue < 0m || LooksLikeCreditDocument(row.DocumentType, row.InvoiceNumber))
|
||||||
@@ -996,10 +1046,14 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildFinanceStatus(decimal? difference)
|
private static string BuildFinanceStatus(decimal? referenceValue, int actualRowCount, decimal? difference)
|
||||||
{
|
{
|
||||||
if (!difference.HasValue)
|
if (!referenceValue.HasValue)
|
||||||
return "Kein Sollwert";
|
return "Kein Sollwert";
|
||||||
|
if (actualRowCount == 0)
|
||||||
|
return "Keine Daten";
|
||||||
|
if (!difference.HasValue)
|
||||||
|
return "Pruefen";
|
||||||
|
|
||||||
return Math.Abs(difference.Value) <= 1m ? "OK" : "Pruefen";
|
return Math.Abs(difference.Value) <= 1m ? "OK" : "Pruefen";
|
||||||
}
|
}
|
||||||
@@ -1793,6 +1847,8 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
public DateTime ExtractionDate { get; set; }
|
public DateTime ExtractionDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record FinanceReferenceValue(string Key, string Label, decimal? Value);
|
||||||
|
|
||||||
private sealed record AggregationSelection(
|
private sealed record AggregationSelection(
|
||||||
ValueFieldDefinition ValueField,
|
ValueFieldDefinition ValueField,
|
||||||
IReadOnlyList<ValueFieldDefinition> AdditionalValueFields,
|
IReadOnlyList<ValueFieldDefinition> AdditionalValueFields,
|
||||||
|
|||||||
@@ -350,6 +350,50 @@ 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_Keeps_Reference_Only_Countries_In_Expert_Mode()
|
||||||
|
{
|
||||||
|
await using (var db = await _dbFactory.CreateDbContextAsync())
|
||||||
|
{
|
||||||
|
db.FinanceReferences.RemoveRange(db.FinanceReferences);
|
||||||
|
db.FinanceReferences.AddRange(
|
||||||
|
new FinanceReference
|
||||||
|
{
|
||||||
|
Key = "DE",
|
||||||
|
Label = "Trafag DE",
|
||||||
|
Year = 2025,
|
||||||
|
LocalCurrencyValue = 120m,
|
||||||
|
IsActive = true
|
||||||
|
},
|
||||||
|
new FinanceReference
|
||||||
|
{
|
||||||
|
Key = "IT",
|
||||||
|
Label = "Trafag IT",
|
||||||
|
Year = 2025,
|
||||||
|
LocalCurrencyValue = 7669840m,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
await SeedCentralRowsAsync(
|
||||||
|
CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "INV-1", "EUR", 100m, new DateTime(2025, 1, 10)));
|
||||||
|
|
||||||
|
var result = await _service.AnalyzeFinanceSummaryAsync(2025, null, null);
|
||||||
|
|
||||||
|
var italy = Assert.Single(result.CountryRows, row => row.CountryKey == "IT");
|
||||||
|
Assert.Equal(7669840m, italy.ReferenceValue);
|
||||||
|
Assert.Equal(0m, italy.NetSalesActual);
|
||||||
|
Assert.Equal(0, italy.TotalRows);
|
||||||
|
Assert.Equal("Keine Daten", italy.Status);
|
||||||
|
Assert.Contains("IT", result.CountryOptions);
|
||||||
|
|
||||||
|
var filteredResult = await _service.AnalyzeFinanceSummaryAsync(2025, "IT", null);
|
||||||
|
var filteredItaly = Assert.Single(filteredResult.CountryRows);
|
||||||
|
Assert.Equal("IT", filteredItaly.CountryKey);
|
||||||
|
Assert.Equal(7669840m, filteredItaly.ReferenceValue);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AnalyzeFinanceSummaryAsync_Builds_Central_Product_Assignment_Tab_Data()
|
public async Task AnalyzeFinanceSummaryAsync_Builds_Central_Product_Assignment_Tab_Data()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user