Keep finance references in expert analysis
This commit is contained in:
@@ -1446,6 +1446,28 @@
|
||||
if (_financeResult is null)
|
||||
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
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
@@ -1453,9 +1475,7 @@
|
||||
group => group.ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var sourceRows = IsFinance3dReferenceYearIndicator(_finance3dIndicator)
|
||||
? _financeResult.Rows
|
||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
||||
var sourceRows = _financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows;
|
||||
|
||||
return sourceRows
|
||||
.OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||
@@ -1481,6 +1501,22 @@
|
||||
if (_financeResult is null)
|
||||
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
|
||||
.GroupBy(row => $"{row.Year}|{row.CountryKey}", StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
@@ -1488,9 +1524,7 @@
|
||||
group => group.ToList(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var sourceRows = IsFinance3dReferenceYearIndicator(_finance3dIndicator)
|
||||
? _financeResult.Rows
|
||||
: (_financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows);
|
||||
var sourceRows = _financeResult.YearCountryRows.Count > 0 ? _financeResult.YearCountryRows : _financeResult.Rows;
|
||||
|
||||
var values = sourceRows
|
||||
.Select(row =>
|
||||
@@ -1529,6 +1563,15 @@
|
||||
_ => 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)
|
||||
=> indicator is Finance3dIndicators.ReferenceValue or Finance3dIndicators.Deviation or Finance3dIndicators.DeviationPercent;
|
||||
|
||||
@@ -1662,6 +1705,17 @@
|
||||
? value.ToString("N2")
|
||||
: $"{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)
|
||||
=> value.HasValue ? FormatValue(value.Value, currency) : "-";
|
||||
|
||||
@@ -1714,6 +1768,8 @@
|
||||
{
|
||||
if (!row.ReferenceValue.HasValue)
|
||||
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")
|
||||
return T("Freigabefaehig.", "Ready for approval.");
|
||||
if (row.Difference.HasValue)
|
||||
|
||||
@@ -390,13 +390,43 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
})
|
||||
.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
|
||||
.Select(row => row.Year)
|
||||
.Concat(references.Select(reference => reference.Year))
|
||||
.Distinct()
|
||||
.OrderBy(yearValue => yearValue)
|
||||
.ToList();
|
||||
if (year == 0)
|
||||
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 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.");
|
||||
}
|
||||
|
||||
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 countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
|
||||
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey, year, countryFilter, currencyFilter);
|
||||
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
|
||||
var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
|
||||
notices.AddRange(BuildProductAssignmentNotices(productAssignmentRows, productFinanceSummary));
|
||||
@@ -480,6 +499,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
YearOptions = yearOptions,
|
||||
CountryOptions = allRows
|
||||
.Select(row => row.CountryKey)
|
||||
.Concat(references.Where(reference => reference.Year == year).Select(reference => reference.Key))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList(),
|
||||
@@ -647,15 +667,18 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
|
||||
private static List<ManagementFinanceCountryStatusRow> BuildFinanceCountryStatusRows(
|
||||
IReadOnlyCollection<FinanceAggregationRow> rows,
|
||||
IReadOnlyDictionary<string, decimal?> referenceByKey)
|
||||
=> rows
|
||||
IReadOnlyDictionary<string, FinanceReferenceValue?> referenceByKey,
|
||||
int year,
|
||||
string? countryFilter,
|
||||
string? currencyFilter)
|
||||
{
|
||||
var actualRows = rows
|
||||
.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 =>
|
||||
{
|
||||
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 intercompanyValue = rowList.Where(row => row.IsIntercompany).Sum(row => row.Value);
|
||||
var difference = referenceValue.HasValue ? actual - referenceValue.Value : (decimal?)null;
|
||||
@@ -674,11 +697,38 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
ReferenceValue = referenceValue,
|
||||
Difference = difference,
|
||||
DifferencePercent = referenceValue is > 0m && difference.HasValue ? difference.Value / referenceValue.Value * 100m : null,
|
||||
Status = BuildFinanceStatus(difference)
|
||||
Status = BuildFinanceStatus(referenceValue, rowList.Count, difference)
|
||||
};
|
||||
})
|
||||
.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)
|
||||
=> rows
|
||||
.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";
|
||||
if (actualRowCount == 0)
|
||||
return "Keine Daten";
|
||||
if (!difference.HasValue)
|
||||
return "Pruefen";
|
||||
|
||||
return Math.Abs(difference.Value) <= 1m ? "OK" : "Pruefen";
|
||||
}
|
||||
@@ -1793,6 +1847,8 @@ public class ManagementCockpitService : IManagementCockpitService
|
||||
public DateTime ExtractionDate { get; set; }
|
||||
}
|
||||
|
||||
private sealed record FinanceReferenceValue(string Key, string Label, decimal? Value);
|
||||
|
||||
private sealed record AggregationSelection(
|
||||
ValueFieldDefinition ValueField,
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task AnalyzeFinanceSummaryAsync_Builds_Central_Product_Assignment_Tab_Data()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user