diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
index 37fda55..7423136 100644
--- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
+++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
@@ -152,6 +152,8 @@
@T("Quelle", "Source")
@T("Waehrung", "Currency")
@T("Ist", "Actual")
+ @T("IC/2nd-party", "IC/2nd-party")
+ @T("Ist ohne IC", "Actual excl. IC")
@T("Soll", "Reference")
@T("Differenz", "Difference")
@T("Zeilen", "Rows")
@@ -163,6 +165,8 @@
@context.SourceSystems
@context.Currency
@FormatValue(context.NetSalesActual, context.Currency)
+ @FormatValue(context.IntercompanyValue, context.Currency)
+ @FormatValue(context.NetSalesActualExcludingIntercompany, context.Currency)
@FormatNullableValue(context.ReferenceValue, context.Currency)
@FormatNullableValue(context.Difference, context.Currency)
@context.IncludedRows.ToString("N0") / @context.ExcludedRows.ToString("N0")
@@ -720,6 +724,7 @@
@T("Laender", "Countries")@_centralResult.Summary.CountryCount.ToString("N0")
@_centralResult.Summary.ValueFieldLabel@FormatValue(_centralResult.Summary.ValueTotal, _centralResult.Summary.DisplayCurrency)
@T("Nicht umgerechnet", "Not converted")@_centralResult.Summary.MissingExchangeRateCount.ToString("N0")
+ @T("Kursdatum", "Rate date")@_centralResult.Summary.ExchangeRateDateLabel
@@ -881,6 +886,7 @@
private List _valueFieldOptions = [];
private readonly List _currencyOptions =
[
+ new(ManagementCockpitCurrencyOptions.Chf, "CHF"),
new(ManagementCockpitCurrencyOptions.Eur, "EUR"),
new(ManagementCockpitCurrencyOptions.Usd, "USD"),
new(ManagementCockpitCurrencyOptions.Native, "Original")
diff --git a/TrafagSalesExporter/Components/Pages/Settings.razor b/TrafagSalesExporter/Components/Pages/Settings.razor
index 040976e..fec4ca4 100644
--- a/TrafagSalesExporter/Components/Pages/Settings.razor
+++ b/TrafagSalesExporter/Components/Pages/Settings.razor
@@ -269,6 +269,15 @@
+
+
+ PostingDate / Buchungsdatum
+ InvoiceDate / Rechnungsdatum
+ ExtractionDate / Extraktionsdatum
+
+
diff --git a/TrafagSalesExporter/Models/ConfigTransferPackage.cs b/TrafagSalesExporter/Models/ConfigTransferPackage.cs
index 890cce3..8253f0f 100644
--- a/TrafagSalesExporter/Models/ConfigTransferPackage.cs
+++ b/TrafagSalesExporter/Models/ConfigTransferPackage.cs
@@ -51,6 +51,7 @@ public class ConfigTransferExportSettings
public bool DebugLoggingEnabled { get; set; }
public string LocalSiteExportFolder { get; set; } = string.Empty;
public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
+ public string ExchangeRateDateField { get; set; } = ExchangeRateDateFields.PostingDate;
}
public class ConfigTransferCurrencyExchangeRate
diff --git a/TrafagSalesExporter/Models/ExportSettings.cs b/TrafagSalesExporter/Models/ExportSettings.cs
index 19594ce..17eb490 100644
--- a/TrafagSalesExporter/Models/ExportSettings.cs
+++ b/TrafagSalesExporter/Models/ExportSettings.cs
@@ -10,4 +10,12 @@ public class ExportSettings
public bool DebugLoggingEnabled { get; set; }
public string LocalSiteExportFolder { get; set; } = string.Empty;
public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
+ public string ExchangeRateDateField { get; set; } = ExchangeRateDateFields.PostingDate;
+}
+
+public static class ExchangeRateDateFields
+{
+ public const string PostingDate = nameof(PostingDate);
+ public const string InvoiceDate = nameof(InvoiceDate);
+ public const string ExtractionDate = nameof(ExtractionDate);
}
diff --git a/TrafagSalesExporter/Models/ManagementCockpitModels.cs b/TrafagSalesExporter/Models/ManagementCockpitModels.cs
index 39e9f4c..a5cf8d7 100644
--- a/TrafagSalesExporter/Models/ManagementCockpitModels.cs
+++ b/TrafagSalesExporter/Models/ManagementCockpitModels.cs
@@ -18,6 +18,7 @@ public static class ManagementCockpitValueFieldKeys
public static class ManagementCockpitCurrencyOptions
{
public const string Native = "NATIVE";
+ public const string Chf = "CHF";
public const string Eur = "EUR";
public const string Usd = "USD";
}
@@ -107,6 +108,8 @@ public class ManagementCockpitCentralSummary
public string DisplayCurrency { get; set; } = string.Empty;
public decimal ValueTotal { get; set; }
public int MissingExchangeRateCount { get; set; }
+ public string ExchangeRateDateField { get; set; } = ExchangeRateDateFields.PostingDate;
+ public string ExchangeRateDateLabel { get; set; } = "PostingDate / Buchungsdatum";
public DateTime? PeriodStart { get; set; }
public DateTime? PeriodEnd { get; set; }
}
@@ -175,6 +178,8 @@ public class ManagementFinanceCountryStatusRow : ManagementFinanceSummaryRow
{
public string SourceSystems { get; set; } = string.Empty;
public string Tscs { get; set; } = string.Empty;
+ public decimal IntercompanyValue { get; set; }
+ public decimal NetSalesActualExcludingIntercompany { get; set; }
public decimal? ReferenceValue { get; set; }
public decimal? Difference { get; set; }
public decimal? DifferencePercent { get; set; }
diff --git a/TrafagSalesExporter/SAGE_SPAIN_EXPORT_2026-05-05.md b/TrafagSalesExporter/SAGE_SPAIN_EXPORT_2026-05-05.md
index 2d37161..014dcb4 100644
--- a/TrafagSalesExporter/SAGE_SPAIN_EXPORT_2026-05-05.md
+++ b/TrafagSalesExporter/SAGE_SPAIN_EXPORT_2026-05-05.md
@@ -1,16 +1,23 @@
# Sage Spain Export
-Stand: 2026-05-05
+Stand: 2026-06-01
+
+Nachtrag 2026-06-01:
+
+- Finance/Andreas bestaetigt: Spanien hat keine echte Ist-Abweichung.
+- Der Wert `3'082'320.18 EUR` ist fachlich plausibel und wird als ES-Referenz 2025 verwendet.
+- Der alte Sollwert `3'102'333.61 EUR` war ein Referenz-/Excel-Fehler.
+- Die historischen Abschnitte unten dokumentieren den frueheren Analysepfad und sind nicht mehr als aktueller ES-Status zu lesen.
## Aktueller Kurzstatus
- Spanien-v2-Export ist technisch lauffaehig und im Testprogramm sichtbar.
- Datei: `sagespain/v2/Spain_Sales_2025.csv`
- Ist 2025: `3'082'320.18` EUR
-- Soll aus `check.xlsx`: `3'102'333.61`
-- Differenz: `-20'013.43`
-- Status FinanceProbe: Gelb / Pruefen
-- Finale Aussage: technisch importierbar, aber fachlich noch nicht abgestimmt.
+- Korrigierte Referenz: `3'082'320.18`
+- Differenz: `0.00`
+- Status FinanceProbe: OK, sofern die korrigierte Referenz geladen ist
+- Finale Aussage: technisch importierbar und laut Sitzung fachlich plausibel; alter Sollwert war falsch.
FinanceProbe lokal:
diff --git a/TrafagSalesExporter/Services/ConfigTransferService.cs b/TrafagSalesExporter/Services/ConfigTransferService.cs
index 6177273..ceb0e85 100644
--- a/TrafagSalesExporter/Services/ConfigTransferService.cs
+++ b/TrafagSalesExporter/Services/ConfigTransferService.cs
@@ -70,7 +70,8 @@ public class ConfigTransferService : IConfigTransferService
TimerEnabled = exportSettings.TimerEnabled,
DebugLoggingEnabled = exportSettings.DebugLoggingEnabled,
LocalSiteExportFolder = exportSettings.LocalSiteExportFolder,
- LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder
+ LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder,
+ ExchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(exportSettings.ExchangeRateDateField)
},
SourceSystemDefinitions = sourceSystems.Select(system => new ConfigTransferSourceSystemDefinition
{
@@ -283,7 +284,8 @@ public class ConfigTransferService : IConfigTransferService
TimerEnabled = importedSettings.TimerEnabled,
DebugLoggingEnabled = importedSettings.DebugLoggingEnabled,
LocalSiteExportFolder = importedSettings.LocalSiteExportFolder,
- LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder
+ LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder,
+ ExchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(importedSettings.ExchangeRateDateField)
});
foreach (var sourceSystem in importedSourceSystems)
diff --git a/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs b/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs
index d5aee8b..dfab745 100644
--- a/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs
+++ b/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs
@@ -27,7 +27,8 @@ CREATE TABLE ExportSettings (
TimerEnabled INTEGER NOT NULL,
DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0,
LocalSiteExportFolder TEXT NOT NULL DEFAULT '',
- LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT ''
+ LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT '',
+ ExchangeRateDateField TEXT NOT NULL DEFAULT 'PostingDate'
);";
internal static string GetHanaServersCreateSql() => @"
diff --git a/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs b/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs
index b5025ee..561fab4 100644
--- a/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs
+++ b/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs
@@ -29,6 +29,7 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''");
+ AddColumnIfMissing(db, "ExportSettings", "ExchangeRateDateField", "TEXT NOT NULL DEFAULT 'PostingDate'");
AddColumnIfMissing(db, "SharePointConfigs", "CentralExportFolder", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''");
EnsureTransformationTable(db);
diff --git a/TrafagSalesExporter/Services/DatabaseSeedService.cs b/TrafagSalesExporter/Services/DatabaseSeedService.cs
index 22c05e4..5bfe819 100644
--- a/TrafagSalesExporter/Services/DatabaseSeedService.cs
+++ b/TrafagSalesExporter/Services/DatabaseSeedService.cs
@@ -57,7 +57,8 @@ public class DatabaseSeedService : IDatabaseSeedService
TimerEnabled = true,
DebugLoggingEnabled = false,
LocalSiteExportFolder = "",
- LocalConsolidatedExportFolder = ""
+ LocalConsolidatedExportFolder = "",
+ ExchangeRateDateField = ExchangeRateDateFields.PostingDate
});
db.SaveChanges();
@@ -868,7 +869,7 @@ public class DatabaseSeedService : IDatabaseSeedService
new FinanceReference { Key = "CN", Label = "Trafag CN", Year = 2025 },
new FinanceReference { Key = "CZ", Label = "Trafag CZ", Year = 2025, LocalCurrencyValue = 95458782m },
new FinanceReference { Key = "DE", Label = "Trafag DE", Year = 2025, LocalCurrencyValue = 3652394.46m },
- new FinanceReference { Key = "ES", Label = "Trafag ES", Year = 2025, LocalCurrencyValue = 3102333.61m },
+ new FinanceReference { Key = "ES", Label = "Trafag ES", Year = 2025, LocalCurrencyValue = 3082320.18m, Notes = "Sitzung 2026-06-01: ES-Ist 3'082'320.18 EUR fachlich bestaetigt; alter Sollwert 3'102'333.61 war Referenz-/Excel-Fehler." },
new FinanceReference { Key = "FR", Label = "Trafag FR", Year = 2025, LocalCurrencyValue = 1450582m, CheckValue = 1471218m },
new FinanceReference { Key = "GFS", Label = "Trafag GfS", Year = 2025, LocalCurrencyValue = 6495513m },
new FinanceReference { Key = "IN", Label = "Trafag IN", Year = 2025, LocalCurrencyValue = 747341702m, CheckValue = 750936591m },
@@ -904,9 +905,11 @@ public class DatabaseSeedService : IDatabaseSeedService
}
}
- if (current.Key == "ES" && current.Year == 2025 && current.LocalCurrencyValue != 3102333.61m)
+ if (current.Key == "ES" && current.Year == 2025 && current.LocalCurrencyValue != 3082320.18m)
{
- current.LocalCurrencyValue = 3102333.61m;
+ current.LocalCurrencyValue = 3082320.18m;
+ current.CheckValue = null;
+ current.Notes = "Sitzung 2026-06-01: ES-Ist 3'082'320.18 EUR fachlich bestaetigt; alter Sollwert 3'102'333.61 war Referenz-/Excel-Fehler.";
changed = true;
}
diff --git a/TrafagSalesExporter/Services/ManagementCockpitService.cs b/TrafagSalesExporter/Services/ManagementCockpitService.cs
index a99f72f..d66098a 100644
--- a/TrafagSalesExporter/Services/ManagementCockpitService.cs
+++ b/TrafagSalesExporter/Services/ManagementCockpitService.cs
@@ -184,6 +184,8 @@ public class ManagementCockpitService : IManagementCockpitService
var aggregation = ResolveAggregation(options);
using var db = await _dbFactory.CreateDbContextAsync();
+ var settings = await db.ExportSettings.AsNoTracking().FirstOrDefaultAsync() ?? new ExportSettings();
+ var exchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
var baseRows = await db.CentralSalesRecords
.Select(r => new CentralCockpitRow
{
@@ -196,10 +198,17 @@ public class ManagementCockpitService : IManagementCockpitService
Quantity = r.Quantity,
StandardCost = r.StandardCost,
SalesValue = r.SalesPriceValue,
- PeriodDate = r.InvoiceDate ?? r.ExtractionDate
+ PostingDate = r.PostingDate,
+ InvoiceDate = r.InvoiceDate,
+ ExtractionDate = r.ExtractionDate,
+ PeriodDate = r.InvoiceDate ?? r.ExtractionDate,
+ ExchangeRateDate = r.ExtractionDate
})
.ToListAsync();
+ foreach (var row in baseRows)
+ row.ExchangeRateDate = ResolveExchangeRateDate(exchangeRateDateField, row.PostingDate, row.InvoiceDate, row.ExtractionDate);
+
if (baseRows.Count == 0)
throw new InvalidOperationException("Die zentrale Tabelle enthält noch keine Datensätze.");
@@ -246,13 +255,15 @@ public class ManagementCockpitService : IManagementCockpitService
DisplayCurrency = BuildDisplayCurrencyLabel(selectedRows.Select(x => x.DisplayCurrency)),
ValueTotal = selectedRows.Sum(x => x.Value),
MissingExchangeRateCount = selectedRows.Count(x => x.MissingExchangeRate),
+ ExchangeRateDateField = exchangeRateDateField,
+ ExchangeRateDateLabel = BuildExchangeRateDateLabel(exchangeRateDateField),
PeriodStart = selectedRows.Min(x => x.PeriodDate),
PeriodEnd = selectedRows.Max(x => x.PeriodDate)
},
AdditionalValueFields = aggregation.AdditionalValueFields
.Select(ToValueFieldOption)
.ToList(),
- Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate), options),
+ Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate), options, exchangeRateDateField),
YearlyTotals = yearlyRows
.GroupBy(x => new { x.PeriodDate.Year, x.DisplayCurrency })
.OrderBy(g => g.Key.Year)
@@ -316,6 +327,10 @@ public class ManagementCockpitService : IManagementCockpitService
if (financeRules.Count == 0)
financeRules = FinanceRuleEngine.CreateDefaultRules().ToList();
+ var intercompanyRules = await db.FinanceIntercompanyRules
+ .AsNoTracking()
+ .Where(rule => rule.IsActive)
+ .ToListAsync();
var financeRuleEngine = new FinanceRuleEngine(financeRules);
var records = await db.CentralSalesRecords
.AsNoTracking()
@@ -374,6 +389,7 @@ public class ManagementCockpitService : IManagementCockpitService
Include = include,
Value = value,
RawSalesValue = record.SalesPriceValue,
+ IsIntercompany = IsIntercompanyCustomer(record, intercompanyRules),
Quantity = record.Quantity,
InvoiceNumber = record.InvoiceNumber,
DocumentType = record.DocumentType,
@@ -439,7 +455,8 @@ public class ManagementCockpitService : IManagementCockpitService
"Diese Sicht verwendet dieselbe FinanceRuleEngine wie das zentrale Excel-Blatt Finance Summary.",
"Jahr, Land und Waehrung werden auf das Endergebnis angewendet.",
"Finance-Jahr basiert auf PostingDate, danach InvoiceDate, danach ExtractionDate; DE-Regeln koennen das Jahr erzwingen.",
- "Include/Exclude, Gutschriften-Negierung und IT-Deduplizierung folgen den gepflegten Finance Regeln."
+ "Include/Exclude, Gutschriften-Negierung und IT-Deduplizierung folgen den gepflegten Finance Regeln.",
+ "Intercompany / 2nd-party wird als Diagnosebetrag ausgewiesen; der Standard-Ist bleibt inklusive dieser Positionen."
};
if (scopedRows.Count == 0)
{
@@ -461,6 +478,7 @@ public class ManagementCockpitService : IManagementCockpitService
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
+ notices.AddRange(BuildProductAssignmentNotices(productAssignmentRows, productFinanceSummary));
return new ManagementFinanceSummaryResult
{
@@ -578,6 +596,7 @@ public class ManagementCockpitService : IManagementCockpitService
var rowList = group.ToList();
referenceByKey.TryGetValue(group.Key.CountryKey, out var referenceValue);
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;
return new ManagementFinanceCountryStatusRow
{
@@ -587,6 +606,8 @@ public class ManagementCockpitService : IManagementCockpitService
IncludedRows = rowList.Count(row => row.Include),
ExcludedRows = rowList.Count(row => !row.Include),
NetSalesActual = actual,
+ IntercompanyValue = intercompanyValue,
+ NetSalesActualExcludingIntercompany = actual - intercompanyValue,
SourceSystems = JoinDistinct(rowList.Select(row => row.SourceSystem)),
Tscs = JoinDistinct(rowList.Select(row => row.Tsc)),
ReferenceValue = referenceValue,
@@ -871,7 +892,34 @@ public class ManagementCockpitService : IManagementCockpitService
};
private static string NormalizeMaterialKey(string value)
- => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return string.Empty;
+
+ var normalized = new string(value
+ .Trim()
+ .ToUpperInvariant()
+ .Where(ch => !char.IsWhiteSpace(ch))
+ .ToArray());
+
+ var withoutLeadingZeros = normalized.TrimStart('0');
+ return string.IsNullOrWhiteSpace(withoutLeadingZeros) ? "0" : withoutLeadingZeros;
+ }
+
+ private static IEnumerable BuildProductAssignmentNotices(
+ IReadOnlyCollection rows,
+ ManagementProductFinanceSummary summary)
+ {
+ if (rows.Count == 0 || summary.TotalValue == 0m)
+ yield break;
+
+ var unresolvedValuePercent = summary.UnassignedValuePercent + summary.MissingReferenceValuePercent;
+ if (unresolvedValuePercent >= 90m)
+ {
+ yield return $"Spartenanalyse auffaellig: {unresolvedValuePercent:N1}% des Umsatzes sind nicht zugeordnet oder nicht im TR-AG-Stamm. Das ist fachlich unplausibel und weist auf Mapping-/Referenzprobleme hin.";
+ yield return "Pruefpunkte Spartenmapping: ProductDivisionRefSet-Fuellung, Join Z.Matnr = P.Matnr, fuehrende Nullen in Materialnummern, lokale Artikelnummern und letzter ZSCHWEIZ-Export.";
+ }
+ }
private static decimal PercentOf(decimal value, decimal total)
=> total == 0m ? 0m : value * 100m / total;
@@ -919,6 +967,43 @@ public class ManagementCockpitService : IManagementCockpitService
return string.Join(", ", reasons.Distinct(StringComparer.OrdinalIgnoreCase));
}
+ private static bool IsIntercompanyCustomer(SalesRecord record, IReadOnlyList rules)
+ {
+ var customerNumber = record.CustomerNumber?.Trim() ?? string.Empty;
+ var customerName = record.CustomerName?.Trim() ?? string.Empty;
+ if (string.IsNullOrWhiteSpace(customerNumber) && string.IsNullOrWhiteSpace(customerName))
+ return false;
+
+ var normalizedCustomerName = NormalizeRuleText(customerName);
+ var referenceKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
+
+ foreach (var rule in rules)
+ {
+ if (!string.IsNullOrWhiteSpace(rule.ScopeKey) &&
+ !rule.ScopeKey.Equals(referenceKey, StringComparison.OrdinalIgnoreCase) &&
+ !rule.ScopeKey.Equals(record.Tsc, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ if (!string.IsNullOrWhiteSpace(rule.CustomerNumber) &&
+ customerNumber.Equals(rule.CustomerNumber.Trim(), StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ if (!string.IsNullOrWhiteSpace(rule.CustomerNameContains) &&
+ normalizedCustomerName.Contains(NormalizeRuleText(rule.CustomerNameContains), StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static string NormalizeRuleText(string value)
+ => (value ?? string.Empty)
+ .Replace("\u00e4", "ae", StringComparison.OrdinalIgnoreCase)
+ .Replace("\u00f6", "oe", StringComparison.OrdinalIgnoreCase)
+ .Replace("\u00fc", "ue", StringComparison.OrdinalIgnoreCase)
+ .Trim()
+ .ToUpperInvariant();
+
private static string JoinDistinct(IEnumerable values)
{
var distinct = values
@@ -1019,7 +1104,7 @@ public class ManagementCockpitService : IManagementCockpitService
.ToList();
var targetCurrency = (options?.TargetCurrency ?? ManagementCockpitCurrencyOptions.Native).Trim().ToUpperInvariant();
- if (targetCurrency is not ManagementCockpitCurrencyOptions.Eur and not ManagementCockpitCurrencyOptions.Usd)
+ if (targetCurrency is not ManagementCockpitCurrencyOptions.Chf and not ManagementCockpitCurrencyOptions.Eur and not ManagementCockpitCurrencyOptions.Usd)
targetCurrency = ManagementCockpitCurrencyOptions.Native;
return new AggregationSelection(
@@ -1047,14 +1132,14 @@ public class ManagementCockpitService : IManagementCockpitService
{
var value = ResolveValue(row, aggregation.ValueField);
var currency = ResolveCurrency(row, aggregation.ValueField);
- var converted = ConvertValue(value, currency, aggregation.ValueField, aggregation, row.PeriodDate);
+ var converted = ConvertValue(value, currency, aggregation.ValueField, aggregation, row.ExchangeRateDate);
var additionalValues = aggregation.AdditionalValueFields.ToDictionary(
field => field.Key,
field =>
{
var additionalValue = ResolveValue(row, field);
var additionalCurrency = ResolveCurrency(row, field);
- return ConvertValue(additionalValue, additionalCurrency, field, aggregation, row.PeriodDate);
+ return ConvertValue(additionalValue, additionalCurrency, field, aggregation, row.ExchangeRateDate);
},
StringComparer.OrdinalIgnoreCase);
@@ -1108,6 +1193,22 @@ public class ManagementCockpitService : IManagementCockpitService
private static string BuildRateCacheKey(string fromCurrency, string toCurrency, DateTime date)
=> $"{fromCurrency}|{toCurrency}|{date:yyyy-MM-dd}";
+ private static DateTime ResolveExchangeRateDate(string exchangeRateDateField, DateTime? postingDate, DateTime? invoiceDate, DateTime extractionDate)
+ => exchangeRateDateField switch
+ {
+ ExchangeRateDateFields.InvoiceDate => invoiceDate ?? postingDate ?? extractionDate,
+ ExchangeRateDateFields.ExtractionDate => extractionDate,
+ _ => postingDate ?? invoiceDate ?? extractionDate
+ };
+
+ private static string BuildExchangeRateDateLabel(string exchangeRateDateField)
+ => exchangeRateDateField switch
+ {
+ ExchangeRateDateFields.InvoiceDate => "InvoiceDate / Rechnungsdatum",
+ ExchangeRateDateFields.ExtractionDate => "ExtractionDate / Extraktionsdatum",
+ _ => "PostingDate / Buchungsdatum"
+ };
+
private static decimal ResolveValue(CockpitRow row, ValueFieldDefinition field)
=> field.Key switch
{
@@ -1161,7 +1262,8 @@ public class ManagementCockpitService : IManagementCockpitService
private static List BuildCentralNotices(
AggregationSelection aggregation,
int missingExchangeRateCount,
- ManagementCockpitAnalysisOptions? options)
+ ManagementCockpitAnalysisOptions? options,
+ string exchangeRateDateField)
{
var notices = new List
{
@@ -1169,7 +1271,8 @@ public class ManagementCockpitService : IManagementCockpitService
$"Summenfeld: {aggregation.ValueField.Label}.",
"Keine Intercompany-Bereinigung angewendet.",
"Kein Budget- und kein Spartemapping angewendet.",
- "Periodenlogik basiert auf Invoice Date, falls vorhanden, sonst auf Extraction Date."
+ "Periodenlogik basiert auf Invoice Date, falls vorhanden, sonst auf Extraction Date.",
+ $"Wechselkurse werden auf {BuildExchangeRateDateLabel(exchangeRateDateField)} angewendet."
};
var landFilter = NormalizeOptionalFilter(options?.LandFilter);
@@ -1561,7 +1664,11 @@ public class ManagementCockpitService : IManagementCockpitService
public decimal Quantity { get; set; }
public decimal StandardCost { get; set; }
public decimal SalesValue { get; set; }
+ public DateTime? PostingDate { get; set; }
+ public DateTime? InvoiceDate { get; set; }
+ public DateTime ExtractionDate { get; set; }
public DateTime PeriodDate { get; set; }
+ public DateTime ExchangeRateDate { get; set; }
}
private class CentralAggregationRow
@@ -1588,6 +1695,7 @@ public class ManagementCockpitService : IManagementCockpitService
public bool Include { get; set; }
public decimal Value { get; set; }
public decimal RawSalesValue { get; set; }
+ public bool IsIntercompany { get; set; }
public decimal Quantity { get; set; }
public string InvoiceNumber { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
diff --git a/TrafagSalesExporter/Services/SettingsPageService.cs b/TrafagSalesExporter/Services/SettingsPageService.cs
index a871384..f58bbee 100644
--- a/TrafagSalesExporter/Services/SettingsPageService.cs
+++ b/TrafagSalesExporter/Services/SettingsPageService.cs
@@ -96,6 +96,7 @@ public sealed class SettingsPageService : ISettingsPageService
var existing = await db.ExportSettings.FirstOrDefaultAsync();
if (existing is null)
{
+ settings.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
db.ExportSettings.Add(settings);
}
else
@@ -107,6 +108,7 @@ public sealed class SettingsPageService : ISettingsPageService
existing.DebugLoggingEnabled = settings.DebugLoggingEnabled;
existing.LocalSiteExportFolder = settings.LocalSiteExportFolder;
existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder;
+ existing.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
}
await db.SaveChangesAsync();
@@ -281,6 +283,18 @@ public sealed class SettingsPageService : ISettingsPageService
public static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
+ public static string NormalizeExchangeRateDateField(string? value)
+ {
+ var normalized = NormalizeConfigValue(value);
+ return normalized switch
+ {
+ ExchangeRateDateFields.PostingDate => ExchangeRateDateFields.PostingDate,
+ ExchangeRateDateFields.InvoiceDate => ExchangeRateDateFields.InvoiceDate,
+ ExchangeRateDateFields.ExtractionDate => ExchangeRateDateFields.ExtractionDate,
+ _ => ExchangeRateDateFields.PostingDate
+ };
+ }
+
public static string BuildSharePointTestPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
{
var maskedSecret = string.IsNullOrEmpty(clientSecret)
diff --git a/TrafagSalesExporter/Tools/FinanceProbe/Program.cs b/TrafagSalesExporter/Tools/FinanceProbe/Program.cs
index 83b1b43..91dafdb 100644
--- a/TrafagSalesExporter/Tools/FinanceProbe/Program.cs
+++ b/TrafagSalesExporter/Tools/FinanceProbe/Program.cs
@@ -556,7 +556,7 @@ static SpainSalesCsvProbe? LoadSpainSalesCsvProbe(string? path)
AddGroupValue(bySeries, series, sales);
}
- const decimal reference = 3102333.61m;
+ const decimal reference = 3082320.18m;
return new SpainSalesCsvProbe
{
Path = path,
diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs
index 0d9d555..6fd1844 100644
--- a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs
+++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs
@@ -171,6 +171,43 @@ public class ManagementCockpitServiceTests : IDisposable
Assert.Equal(1, exchangeRates.ResolveRateCallCount);
}
+ [Fact]
+ public async Task AnalyzeCentralAsync_Uses_Configured_Exchange_Rate_Date_Field()
+ {
+ var exchangeRates = new CountingCurrencyExchangeRateService();
+ var service = new ManagementCockpitService(_dbFactory, exchangeRates);
+
+ await using (var db = await _dbFactory.CreateDbContextAsync())
+ {
+ db.ExportSettings.Add(new ExportSettings
+ {
+ DateFilter = "2025-01-01",
+ ExchangeRateDateField = ExchangeRateDateFields.InvoiceDate
+ });
+ await db.SaveChangesAsync();
+ }
+
+ await SeedCentralRowsAsync(
+ CreateRow(
+ "SAP",
+ "USA",
+ "TRUS",
+ "INV-1",
+ "USD",
+ 100m,
+ new DateTime(2025, 2, 10),
+ postingDate: new DateTime(2025, 1, 10)));
+
+ var result = await service.AnalyzeCentralAsync(2025, 2, new ManagementCockpitAnalysisOptions
+ {
+ ValueField = ManagementCockpitValueFieldKeys.SalesPriceValue,
+ TargetCurrency = ManagementCockpitCurrencyOptions.Eur
+ });
+
+ Assert.Equal(ExchangeRateDateFields.InvoiceDate, result.Summary.ExchangeRateDateField);
+ Assert.Equal(new DateTime(2025, 2, 10), Assert.Single(exchangeRates.EffectiveDates));
+ }
+
[Fact]
public async Task AnalyzeCentralAsync_Can_Sum_Quantity_Without_Currency_Conversion()
{
@@ -318,7 +355,7 @@ public class ManagementCockpitServiceTests : IDisposable
{
await SeedCentralRowsAsync(
CreateRow("SAP", "Schweiz", "ZSCHWEIZ", "CH-1", "CHF", 100m, new DateTime(2025, 1, 10),
- material: "MAT-OK",
+ material: "000MAT-OK",
name: "Reference article",
productHierarchyCode: "0414",
productHierarchyText: "Industat innen",
@@ -393,6 +430,32 @@ public class ManagementCockpitServiceTests : IDisposable
Assert.Equal(80m, deFinanceCoverage.AssignedValuePercent);
}
+ [Fact]
+ public async Task AnalyzeFinanceSummaryAsync_Warns_When_Product_Assignment_Coverage_Is_Implausibly_Low()
+ {
+ await SeedCentralRowsAsync(
+ CreateRow("SAP", "Schweiz", "ZSCHWEIZ", "CH-1", "CHF", 10m, new DateTime(2025, 1, 10),
+ material: "MAT-OK",
+ productHierarchyCode: "0414",
+ productFamilyCode: "0004",
+ productDivisionCode: "0001",
+ productDivisionText: "Thermostate",
+ productMappingAssigned: "X"),
+ CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "DE-1", "EUR", 90m, new DateTime(2025, 1, 11),
+ material: "MAT-MISSING"));
+
+ var result = await _service.AnalyzeFinanceSummaryAsync(2025, null, null);
+
+ Assert.Equal(100m, result.ProductFinanceSummary.TotalValue);
+ Assert.Equal(90m, result.ProductFinanceSummary.MissingReferenceValue);
+ Assert.Contains(result.Notices, notice =>
+ notice.Contains("Spartenanalyse auffaellig", StringComparison.OrdinalIgnoreCase) &&
+ notice.Contains("90.0%", StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(result.Notices, notice =>
+ notice.Contains("ProductDivisionRefSet", StringComparison.OrdinalIgnoreCase) &&
+ notice.Contains("fuehrende Nullen", StringComparison.OrdinalIgnoreCase));
+ }
+
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
{
await using var db = await _dbFactory.CreateDbContextAsync();
@@ -440,7 +503,8 @@ public class ManagementCockpitServiceTests : IDisposable
string productFamilyText = "",
string productDivisionCode = "",
string productDivisionText = "",
- string productMappingAssigned = "")
+ string productMappingAssigned = "",
+ DateTime? postingDate = null)
{
return new CentralSalesRecord
{
@@ -476,6 +540,7 @@ public class ManagementCockpitServiceTests : IDisposable
SalesCurrency = currency,
Incoterms2020 = "DAP",
SalesResponsibleEmployee = "Alice",
+ PostingDate = postingDate,
InvoiceDate = invoiceDate,
OrderDate = invoiceDate?.AddDays(-2),
Land = land,
@@ -501,10 +566,12 @@ public class ManagementCockpitServiceTests : IDisposable
private sealed class CountingCurrencyExchangeRateService : ICurrencyExchangeRateService
{
public int ResolveRateCallCount { get; private set; }
+ public List EffectiveDates { get; } = [];
public decimal? ResolveRate(string fromCurrency, string toCurrency, DateTime? effectiveDate)
{
ResolveRateCallCount++;
+ EffectiveDates.Add(effectiveDate);
return 2m;
}
diff --git a/TrafagSalesExporter/docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md b/TrafagSalesExporter/docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md
index 4f4d833..641e720 100644
--- a/TrafagSalesExporter/docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md
+++ b/TrafagSalesExporter/docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md
@@ -1,6 +1,12 @@
# Finance Berechnungsformeln pro Land
-Stand: 2026-05-19
+Stand: 2026-06-01
+
+Nachtrag 2026-06-01:
+
+- ES-Referenz 2025 wurde nach Finance-Sitzung auf `3'082'320.18 EUR` korrigiert. Der alte Wert `3'102'333.61 EUR` war ein Referenz-/Excel-Fehler.
+- In Management-Analysen ist das Wechselkurs-Anwendungsdatum konfigurierbar: `PostingDate`, `InvoiceDate` oder `ExtractionDate`.
+- Sparten-Materialabgleich normalisiert fuehrende Nullen und warnt bei >=90% ungeklaerter Abdeckung.
Zweck: Dieses Dokument beschreibt die aktuell im Programm verwendeten Formeln fuer den Soll/Ist-Vergleich 2025. Es ist fuer eine zweite KI oder eine fachliche Gegenpruefung geschrieben.
@@ -92,7 +98,7 @@ Der IC-Abzug veraendert die Originaldaten und den Haupt-Ist-Wert nicht.
| Schweiz | CH | SAP OData `ZSCHWEIZ`, falls importiert | CHF | leer | kein Sollwert im Seed |
| Oesterreich | AT | SAP OData `ZSCHWEIZ`, falls importiert | EUR | 3'443'863 | gemeinsame Logik |
| Deutschland | DE | nur falls Daten in `CentralSalesRecords` vorhanden | EUR | 3'635'923 | gemeinsame Logik |
-| Spanien | ES | Sage SQL CSV / Manual Excel | EUR | 3'102'333.61 | SalesPriceValue aus Sage `ImporteNeto` |
+| Spanien | ES | Sage SQL CSV / Manual Excel | EUR | 3'082'320.18 | SalesPriceValue aus Sage `ImporteNeto` |
| Frankreich | FR | SAP B1/HANA Schema `fr01_p` | EUR | CheckValue 1'471'218 | SalesPriceValue / B1 Positions-Netto |
| Indien | IN | Sage/HANA `TRAFAG_LIVE` | INR | CheckValue 750'936'591 | Hauswaehrung INR |
| Italien | IT | SAP B1/HANA Schema `it01_p` | EUR | 7'669'840 | B1 Positions-Netto mit provisorischem Filter |
@@ -250,22 +256,21 @@ Formel im Vergleich:
```text
Ist ES = Sum(SalesPriceValue)
-Soll ES = 3'102'333.61 EUR
+Soll ES = 3'082'320.18 EUR
```
Bekannter Stand:
```text
Ist ca. 3'082'320.18 EUR
-Differenz ca. -20'013.43 EUR
+Differenz ca. 0.00 EUR
```
Offen:
```text
-Abweichung ist ca. 0.65%.
-Wahrscheinliche Pruefpunkte: Fracht, Portes, Zuschlaege, Rundungen, Versicherung,
-Finanzierung, nicht-artikelbezogene Belegpositionen oder abweichende Rhino-Auswertung.
+Die fruehere Abweichung entstand aus einem falschen Soll-/Referenzwert.
+Falls Audit gefragt ist, muss die Herkunft des alten Werts 3'102'333.61 EUR nachvollzogen werden.
```
## FR
diff --git a/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md b/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md
index 25e9c6c..cb8f065 100644
--- a/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md
+++ b/TrafagSalesExporter/docs/FINANCE_ENTSCHEIDE.md
@@ -1,6 +1,21 @@
# Finance-Entscheide fuer Net Sales Actuals
-Stand: 2026-05-20
+Stand: 2026-06-01
+
+## Nachtrag 2026-06-01 Finance-Sitzung
+
+Umgesetzt:
+
+- ES-Referenz 2025 ist auf `3'082'320.18 EUR` korrigiert; alter Sollwert `3'102'333.61 EUR` war Referenz-/Excel-Fehler.
+- `Management Analyse > Laender` zeigt IC/2nd-party und `Ist ohne IC` als Diagnosewerte.
+- Wechselkurs-Anwendungsdatum ist in den Settings konfigurierbar und wird in der Rohdaten-Diagnose angezeigt.
+- Sparten-Materialabgleich normalisiert fuehrende Nullen; bei >=90% nicht zugeordnet / nicht im TR-AG-Stamm wird ein Warnhinweis angezeigt.
+
+Weiter fachlich zu klaeren:
+
+- Pro Standort bestaetigen, ob Intercompany bereits in der gelieferten Quelle herausgerechnet ist.
+- Fuer Wechselkurse final bestaetigen, ob `PostingDate`, `InvoiceDate` oder ein anderes Datum fuehrend ist.
+- Spartenanalyse fachlich pruefen, falls die ungeklaerte Abdeckung weiterhin extrem hoch bleibt.
## Nachtrag 2026-05-20 Finance Summary / Management Analyse
@@ -91,7 +106,7 @@ Ergebnis im Reporting:
- IT: IC-Kundenliste final bestaetigen.
- CH / AT: echtes SAP-Buchungsdatum pruefen, falls `ZSCHWEIZ` aktuell nur Fakturadatum liefert.
- DE: finaler Alphaplan-Jahresfile liegt vor und ist technisch mappbar. Rohsumme `NettoPreisGesamtX` komplett ist `4'154'690.05 EUR`; nur `Land Kunde = Deutschland` ist `3'455'276.64 EUR`; Sollwert ist `3'635'923.00 EUR`. Offene Fachfrage: welche Kundenlaender/Abgrenzungen gehoeren offiziell zu DE?
-- ES: Aktuell `3'082'320.18 EUR` gegen Soll `3'102'333.61`; Differenz `-20'013.43 EUR`. CSV nutzt `ImporteNeto`; Credit Notes/REC sind negativ. Offen bleiben Perioden-/Serienabgrenzung und ob Rhino eine andere Sage-Auswertung nutzt.
+- ES: `3'082'320.18 EUR` ist laut Sitzung fachlich plausibel und entspricht der korrigierten Referenz. CSV nutzt `ImporteNeto`; Credit Notes/REC sind negativ. Der fruehere Sollwert `3'102'333.61` war ein Referenz-/Excel-Fehler.
## Pruefstand 2026-05-11
diff --git a/TrafagSalesExporter/docs/FINANCE_MEMO_ANDREAS_2026-06-01.md b/TrafagSalesExporter/docs/FINANCE_MEMO_ANDREAS_2026-06-01.md
new file mode 100644
index 0000000..390cce0
--- /dev/null
+++ b/TrafagSalesExporter/docs/FINANCE_MEMO_ANDREAS_2026-06-01.md
@@ -0,0 +1,127 @@
+# Finance Dashboard - Kurzmemo fuer Andreas
+
+Stand: 2026-06-01
+
+## Aktueller Stand
+
+- `Finance Summary` ist die fuehrende Sicht fuer Soll/Ist.
+- `Management Analyse` ist die Diagnoseebene fuer Laender, Datenstatus, Abweichungen, Gutschriften, Datenqualitaet, Spartenanalyse und Rohdaten.
+- Das Dashboard ist technisch produktiv nutzbar.
+- Letzter dokumentierter Testlauf: `80/80` Tests gruen.
+- Standard-Ist bleibt inklusive aller Positionen.
+- Intercompany / 2nd-party wird separat ausgewiesen, aber nicht automatisch herausgerechnet.
+
+## Sitzungsergebnis 2026-06-01
+
+- Spanien hat laut Sitzung keine echte Ist-Abweichung.
+- ES-Ist `3'082'320.18 EUR` ist fachlich plausibel.
+- Der bisherige ES-Sollwert `3'102'333.61 EUR` war falsch bzw. wahrscheinlich ein Excel-/Referenzfehler.
+- ES-Referenz 2025 ist technisch auf `3'082'320.18 EUR` korrigiert.
+- Intercompany ist in einzelnen Standortzahlen anscheinend bereits bereinigt, muss aber pro Standort bestaetigt werden.
+- `Management Analyse > Laender` zeigt nun IC/2nd-party und `Ist ohne IC` als Diagnose.
+- Bei den 2025-Wechselkursen ist das Anwendungsdatum jetzt in den Settings konfigurierbar.
+- In der Sparten-Finanzanalyse sind mehr als 90% nicht zugeordnet; Andreas sagt, das kann fachlich nicht stimmen.
+- Der Materialabgleich normalisiert jetzt fuehrende Nullen und zeigt bei >=90% ungeklaerter Spartenabdeckung einen Warnhinweis.
+
+## Fuehrende Regeln
+
+| Thema | Regel |
+| --- | --- |
+| Vergleich | Je Land in Hauswaehrung |
+| Wertbasis | Nettofakturawert pro Position |
+| Jahresabgrenzung | `PostingDate`, sonst `InvoiceDate`, sonst `ExtractionDate` |
+| Gutschriften / Storno | Negative Beleg-/Positionszeilen |
+| CHF | Reporting-/Kontrollsicht, nicht Standardvergleich |
+| Intercompany | Separat ausweisen, nicht still entfernen |
+
+## Wichtig fuer die Diskussion
+
+### 1. Lokaler Soll/Ist zuerst in Hauswaehrung
+
+Beispiele:
+
+- UK in `GBP`
+- Indien in `INR`
+- USA in `USD`
+- EUR-Laender in `EUR`
+
+Erst wenn die lokale Zahl stimmt, ist eine konsolidierte CHF-Sicht sinnvoll.
+
+### 2. CHF als separate Management-Sicht
+
+Offen ist der offizielle Kurstyp:
+
+- Budgetkurs
+- Monatskurs
+- Transaktionskurs aus ERP
+- Konzern-/Treasury-Kurs
+- Stichtagskurs
+
+Zusaetzlich offen:
+
+- Auf welches Datum soll der Kurs fachlich final angewendet werden?
+- `DocDate`?
+- `PostingDate`?
+- `InvoiceDate`?
+- anderes Periodendatum?
+
+Ohne offiziellen Kurstyp ist eine CHF-Zahl technisch berechenbar, aber fachlich nicht sauber verteidigbar.
+
+Umsetzung: In den Settings gibt es `Wechselkurse anwenden auf`; die Rohdaten-Diagnose zeigt das verwendete Kursdatum an.
+
+### 3. Kosten nicht mit Umsatzfreigabe vermischen
+
+Kosten / Marge sollten als separate Ausbaustufe behandelt werden.
+
+Zu klaeren:
+
+- Standardkosten?
+- Ist-Kosten?
+- Group Cost?
+- Budgetkosten?
+- Finance-Kostentabelle?
+
+Solange die Kostenquelle nicht freigegeben ist, sollte keine offizielle Marge ausgewiesen werden.
+
+## Offene Laenderpunkte
+
+| Land | Offener Punkt |
+| --- | --- |
+| DE | Welche Kundenlaender / Filter gehoeren offiziell zum deutschen Ist? |
+| ES | Keine echte Ist-Abweichung laut Sitzung; Sollwert technisch auf `3'082'320.18 EUR` korrigiert |
+| UK | Sage-Differenz ca. `-5.3k GBP`; Discounts, Freight, Charges und 2nd-party klaeren |
+| IT | Fachliche Methode dokumentiert; neuer Export und finale Abgrenzung pruefen |
+| CH / AT | Klaeren, ob `FKDAT` als Periodendatum akzeptiert ist |
+
+## Offene Strukturpunkte
+
+| Thema | Punkt |
+| --- | --- |
+| Intercompany | Pro Standort klaeren, ob IC bereits in der Quelle herausgerechnet ist; Dashboard zeigt IC-Diagnose |
+| Wechselkurse | Kursanwendungsdatum ist konfigurierbar; fachliche Finalfreigabe fehlt |
+| Spartenanalyse | >90% nicht zugeordnet ist fachlich unplausibel; Mapping / TR-AG-Referenz trotz technischer Normalisierung pruefen |
+
+## Entscheidbedarf von Finance
+
+Finance sollte pro Land bestaetigen:
+
+- Quelle
+- Datum
+- Wertfeld
+- Waehrung
+- Filter
+- Intercompany-Behandlung
+
+Zusaetzlich braucht es Entscheide zu:
+
+- offiziellem CHF-Kurstyp
+- Datumsfeld fuer CHF-Kursanwendung
+- Kurstabelle / Kursquelle
+- Kostenumfang im Dashboard
+- Behandlung kleiner Restabweichungen
+- Korrektur ES-Sollwert
+- Pruefung der Sparten-Zuordnung
+
+## Kernaussage
+
+Das technische Fundament steht. Die wichtigsten naechsten Punkte sind Referenzkorrekturen, fachliche Abgrenzungen und Mapping-Pruefungen, nicht primaer technische Grundlagenprobleme.
diff --git a/TrafagSalesExporter/docs/FINANCE_STATUS_OFFENE_PUNKTE_2026-06-01.md b/TrafagSalesExporter/docs/FINANCE_STATUS_OFFENE_PUNKTE_2026-06-01.md
index 0b33a4b..4e6fbfe 100644
--- a/TrafagSalesExporter/docs/FINANCE_STATUS_OFFENE_PUNKTE_2026-06-01.md
+++ b/TrafagSalesExporter/docs/FINANCE_STATUS_OFFENE_PUNKTE_2026-06-01.md
@@ -10,6 +10,37 @@ Das Finance Dashboard ist technisch produktiv nutzbar. Die fuehrende Sicht ist `
Offen sind nicht primaer technische Grundlagen, sondern fachliche Abgrenzungen je Land: Welche lokale Auswertung ist offiziell fuehrend, welche Filter gelten, und ob bestimmte Differenzen akzeptiert oder durch zusaetzliche Quell-/Filterlogik erklaert werden muessen.
+## Nachtrag Sitzung 2026-06-01
+
+Aus der Sitzung mit Finance / Andreas ergeben sich diese aktualisierten Punkte:
+
+1. Intercompany
+ - Frage aus Finance: Sind Intercompany-Umsaetze bereits in den Standortdaten herausgerechnet?
+ - Aktueller Eindruck aus der Sitzung: Anscheinend sind IC-Anteile in einzelnen Standortauswertungen bereits bereinigt, trotzdem bleiben Abweichungen.
+ - Umsetzung: `Management Analyse > Laender` zeigt jetzt IC/2nd-party und `Ist ohne IC` als Diagnosewerte.
+ - Wichtig fuer die App: Das Dashboard entfernt IC weiterhin nicht automatisch aus dem Standard-Ist.
+ - Folgeaktion: Pro Standort klaeren, ob die Quellzahl bereits netto ohne IC geliefert wird oder ob das Dashboard IC noch fachlich abziehen soll.
+
+2. Spanien
+ - Aussage Sitzung: Spanien hat fachlich keine echte Soll/Ist-Abweichung.
+ - Ist-Wert im Dashboard: `3'082'320.18 EUR`.
+ - Der bisherige Sollwert `3'102'333.61 EUR` ist falsch bzw. wahrscheinlich ein Excel-/Referenzfehler.
+ - Umsetzung: ES-FinanceReference 2025 wird auf `3'082'320.18 EUR` gesetzt; `FinanceProbe` nutzt denselben Referenzwert.
+ - Folgeaktion: Quelle der falschen Excel-/Referenzzahl weiterhin fachlich nachvollziehen, falls Audit gefragt ist.
+
+3. Wechselkurse 2025
+ - In den Settings / Kurstabellen fehlt ein Feld, auf welches Datum der Kurs angewendet wird.
+ - Zu klaeren: Anwendung auf `DocDate`, `PostingDate`, `InvoiceDate` oder ein anderes Periodendatum.
+ - Umsetzung: In `Settings > Export Einstellungen` ist `Wechselkurse anwenden auf` konfigurierbar.
+ - Umsetzung: `Management Analyse > Rohdaten Diagnose` zeigt das verwendete Kursdatum an.
+
+4. Sparten-Finanzanalyse
+ - Aktueller Befund: Mehr als 90% der Werte sind nicht zugeordnet.
+ - Aussage Andreas: Das kann fachlich nicht stimmen.
+ - Umsetzung: Sparten-Materialabgleich normalisiert fuehrende Nullen in Materialnummern.
+ - Umsetzung: Bei >=90% nicht zugeordnet / nicht im Stamm zeigt die Management-Analyse einen Warnhinweis mit Pruefpunkten.
+ - Folgeaktion: Zentrale Spartenzuordnung pruefen, insbesondere Mapping gegen TR-AG-/SAP-Referenz, Materialnummernformat, fuehrende Nullen, lokale Artikelnummern und Fuellung von `ProductDivisionRefSet`.
+
## Was aktuell vorhanden ist
### Finance Summary
@@ -200,7 +231,7 @@ Kosten sollten nicht direkt in die aktuelle Umsatzfreigabe gemischt werden. Sinn
| --- | --- | --- |
| CH / AT | SAP OData `ZSCHWEIZ`; Trennung ueber Buchungskreis / Reporting-Land | Pruefen, ob `FKDAT` fachlich als Buchungsdatum akzeptiert ist |
| DE | Alphaplan Excel; `NettoPreisGesamtX`; finaler 2025-File liegt technisch vor | Finance muss bestaetigen, welche Kundenlaender / Filter zum offiziellen DE-Ist gehoeren |
-| ES | Sage CSV; `ImporteNeto`; Credit Notes / REC negativ | Differenz ca. `-20'013.43 EUR` gegen Soll fachlich klaeren |
+| ES | Sage CSV; `ImporteNeto`; Credit Notes / REC negativ; Ist `3'082'320.18 EUR` fachlich bestaetigt | Bisheriger Sollwert `3'102'333.61 EUR` ist falsch bzw. Excel-/Referenzfehler |
| FR | SAP B1/HANA; Positions-Netto passt praktisch gegen Soll | Kein grosser offener Punkt dokumentiert |
| IN | Hauswaehrung INR; Vergleich in INR | Keine CHF-Tageskurslogik fuer Standardvergleich verwenden |
| IT | SAP B1/HANA; Finance-Methode mit IT-Abgrenzung dokumentiert | Nach neuem IT-Export pruefen, ob Summe und Abgrenzung final passen |
@@ -256,19 +287,18 @@ Aktueller Stand:
- `ImporteNeto` wird als Nettozeile verwendet.
- Credit Notes / REC laufen negativ.
- Ist aktuell ca. `3'082'320.18 EUR`.
-- Sollwert ca. `3'102'333.61 EUR`.
-- Differenz ca. `-20'013.43 EUR`.
+- Sitzung 2026-06-01: Dieser Ist-Wert entspricht fachlich dem erwarteten Wert.
+- Der bisherige Sollwert ca. `3'102'333.61 EUR` ist falsch bzw. wahrscheinlich ein Excel-/Referenzfehler.
Klaerung:
-- Ist `FechaFactura` das richtige Periodendatum?
-- Sind alle Serien enthalten (`REG`, `LAT`, `PRO`, `REC`)?
-- Gibt es Fracht, Portes, Zuschlaege, Versicherung, Finanzierung oder Nebenpositionen, die Rhino anders behandelt?
-- Gibt es eine offizielle Sage-Auswertung, die den Sollwert erzeugt, inklusive Filterbeschreibung?
+- Falschen Sollwert in `check.xlsx` / FinanceReference korrigieren.
+- Klaeren, woher der falsche Sollwert `3'102'333.61 EUR` kam.
+- Danach ES nicht mehr als fachliche Abweichung fuehren, sofern die Referenz korrigiert ist.
Argument:
-Spanien ist technisch angebunden. Die Restabweichung ist klein genug fuer eine gezielte fachliche Filterpruefung, aber noch nicht fachlich freigegeben.
+Spanien ist technisch angebunden und fachlich plausibel. Die bisherige Abweichung entsteht aus einer falschen Soll-/Referenzzahl, nicht aus dem Sage-Ist.
### 4. UK
@@ -317,6 +347,7 @@ Die Datenquelle ist angebunden. Der kritische Punkt ist, ob Finance die aktuelle
- UK ist Sage, nicht SAP B1.
- DE ist Alphaplan, nicht SAP B1.
- Spartenanalyse nutzt TR-AG-/SAP-Referenz als zentrale Wahrheit.
+- Wenn in der Sparten-Finanzanalyse mehr als 90% nicht zugeordnet sind, ist das nicht als fachliche Wahrheit zu akzeptieren, sondern als Mapping-/Referenzproblem zu pruefen.
- Budget-CHF ist Kontrollsicht, nicht Standardabgleich.
- Eine Zahl, die zufaellig naeher am Soll ist, ist nicht automatisch die richtige fachliche Methode.
@@ -334,6 +365,8 @@ Die Datenquelle ist angebunden. Der kritische Punkt ist, ob Finance die aktuelle
- Hauswaehrung bleibt fuehrend fuer lokale Freigabe
- CHF als separate Reporting-Sicht
- offizieller Kurstyp und Kurstabelle
+ - Datumsfeld fuer Kursanwendung, z. B. `DocDate`, `PostingDate` oder `InvoiceDate`
+ - Anzeige im Dashboard, welches Datum fuer den Kurs verwendet wird
3. Finance entscheidet den Kostenumfang:
- vorerst keine offizielle Kosten-KPI
@@ -342,9 +375,10 @@ Die Datenquelle ist angebunden. Der kritische Punkt ist, ob Finance die aktuelle
4. Finance priorisiert die offenen Laender:
- DE: Kundenlaender / Filter
- - ES: Sage-Differenz
+ - ES: Referenz-/Sollwert korrigieren, keine echte Ist-Abweichung laut Sitzung
- UK: Sage-Differenz
- IT: neuer Export und finale Abgrenzung
+ - Spartenanalyse: >90% nicht zugeordnet fachlich unplausibel, Mapping pruefen
5. Finance liefert fuer jede offene Differenz entweder:
- offizielle Reportfilter,
@@ -363,6 +397,9 @@ Die Datenquelle ist angebunden. Der kritische Punkt ist, ob Finance die aktuelle
8. Welcher Kurstyp ist fuer CHF verbindlich?
9. Sollen Kosten jetzt Bestandteil des Finance Dashboards werden oder separat als naechste Ausbaustufe?
10. Welche Kostenquelle waere fachlich fuehrend?
+11. Sind Intercompany-Umsaetze in den Standortquellen bereits herausgerechnet?
+12. Auf welches Datum muessen 2025-Wechselkurse angewendet werden?
+13. Warum sind in der Sparten-Finanzanalyse mehr als 90% nicht zugeordnet, obwohl Andreas das fachlich ausschliesst?
## Quellen im Repo
diff --git a/TrafagSalesExporter/docs/rag/FINANCE.md b/TrafagSalesExporter/docs/rag/FINANCE.md
index fd59605..4840ded 100644
--- a/TrafagSalesExporter/docs/rag/FINANCE.md
+++ b/TrafagSalesExporter/docs/rag/FINANCE.md
@@ -1,6 +1,6 @@
# RAG Finance
-Stand: 2026-05-29
+Stand: 2026-06-01
## Kurzstand
@@ -15,6 +15,10 @@ Stand: 2026-05-29
- Finance-Schulung dokumentiert die neuen Spartenfunktionen im Tab `Spartenanalyse`.
- Filter fuer Jahr, Land und Waehrung wirken auf das Finance-Endergebnis.
- Standard-Ist bleibt inklusive Positionen; Intercompany/2nd-party wird separat ausgewiesen.
+- Nach Sitzung 2026-06-01: ES-Referenz 2025 ist auf `3'082'320.18 EUR` korrigiert; alter Sollwert `3'102'333.61 EUR` war Referenz-/Excel-Fehler.
+- Management Analyse zeigt in `Laender` jetzt IC/2nd-party und `Ist ohne IC` als Diagnose.
+- Wechselkurs-Anwendungsdatum ist in Settings konfigurierbar und wird in der Rohdaten-Diagnose angezeigt.
+- Spartenanalyse war mit >90% nicht zugeordnet fachlich unplausibel; Materialabgleich normalisiert fuehrende Nullen und warnt bei >=90% ungeklaerter Abdeckung.
## Wichtige Regeln
@@ -28,12 +32,13 @@ Stand: 2026-05-29
- DE: Finance/Munir muss bestaetigen, welche Kundenlaender/Filter zum offiziellen DE-Ist gehoeren.
- IT: Nach neuem IT-Export pruefen, ob die vollstaendige `Trafag Italia`-Summe sichtbar wird.
-- ES: Differenz zu Rhino/check.xlsx bleibt fachlich zu klaeren.
+- UK: Sage-Restdifferenz ueber Exportvollstaendigkeit, Discounts, Freight/Charges und 2nd-party klaeren.
+- Spartenanalyse: Falls weiterhin >90% nicht zugeordnet, TR-AG-Referenz/Join/Materialnummern pruefen.
## Management-Analyse-Reiter
- `Finance Summary`: KPI-Karten und Summen wie im zentralen Excel.
-- `Laender`: Ist, Soll, Differenz, Status, Quelle und TSC je Land/Waehrung.
+- `Laender`: Ist, IC/2nd-party, Ist ohne IC, Soll, Differenz, Status, Quelle und TSC je Land/Waehrung.
- `Datenstatus`: Standortbestand, letzte Speicherung, letzter Export, Manual-Import-Hinweise.
- `Abweichungen`: Soll/Ist-Abweichungen sortiert nach Betrag.
- `Gutschriften`: technische Kandidaten ueber negative Werte und erkennbare Belegtypen/-nummern.
@@ -63,7 +68,7 @@ Stand: 2026-05-29
| --- | --- |
| CH/AT | SAP OData `ZSCHWEIZ`, Trennung ueber Buchungskreis/Reporting-Land |
| DE | Alphaplan Excel, `NettoPreisGesamtX`, 2025-Zwang |
-| ES | Sage CSV, `ImporteNeto`, REC/Credit negativ |
+| ES | Sage CSV, `ImporteNeto`, REC/Credit negativ; Referenz 2025 korrigiert auf `3'082'320.18 EUR` |
| IT | Hauswaehrung, `Trafag Italia` ausgeschlossen, Duplikatlogik fuer leeres Supplier country |
| UK | Sage/Manual Excel, GBP, `[Sales Price/Value] * [Quantity]`, Credit Notes negativ |
| IN | INR als Hauswaehrung |
diff --git a/TrafagSalesExporter/lastchange.md b/TrafagSalesExporter/lastchange.md
index 046e2ce..2a97f21 100644
--- a/TrafagSalesExporter/lastchange.md
+++ b/TrafagSalesExporter/lastchange.md
@@ -1,6 +1,6 @@
# Last Change
-Stand: 2026-05-29
+Stand: 2026-06-01
Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
@@ -8,8 +8,16 @@ Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
- Fuehrender Kurzkontext: `docs/rag/PROJECT.md`.
- Themenrouter: `docs/RAG_ROUTER.md`.
-- Letzter dokumentierter Code-Stand: `36ca822 Add browser favicon`, alle Aenderungen bis 2026-05-29 13:47 deployt.
-- Letzte dokumentierte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-favicon` mit `80/80` Tests gruen.
+- Letzter dokumentierter Code-Stand: Finance-Sitzungsnachtrag 2026-06-01 noch nicht deployt.
+- Letzte dokumentierte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-finance-session-proof` mit `82/82` Tests gruen.
+- Neu umgesetzt: ES-Referenz 2025 auf `3'082'320.18 EUR` korrigiert; alter Sollwert `3'102'333.61 EUR` als Referenz-/Excel-Fehler dokumentiert.
+- Neu umgesetzt: `FinanceProbe` nutzt dieselbe korrigierte ES-Referenz.
+- Neu umgesetzt: Wechselkurs-Anwendungsdatum in Settings konfigurierbar (`PostingDate`, `InvoiceDate`, `ExtractionDate`) und in Rohdaten-Diagnose sichtbar.
+- Neu umgesetzt: CHF als Anzeige-Waehrung in Management Analyse verfuegbar.
+- Neu umgesetzt: `Management Analyse > Laender` zeigt IC/2nd-party und `Ist ohne IC` als Diagnosewerte.
+- Neu umgesetzt: Sparten-Materialabgleich normalisiert fuehrende Nullen.
+- Neu umgesetzt: Warnhinweis bei >=90% nicht zugeordnet / nicht im TR-AG-Stamm, mit Test abgesichert.
+- Neu erstellt: kompaktes Andreas-Memo `docs/FINANCE_MEMO_ANDREAS_2026-06-01.md`.
- Neu dokumentiert: Produktsparten-Mapping fuer Group Sales Report ueber TR-AG-Artikelstamm und separate Mapping-Tabelle.
- Neu dokumentiert: Upgreat-Firewall-Freigabe muss fuer den publizierten Webserver `10.120.1.17` erfolgen, nicht fuer den lokalen Entwicklungs-PC.
- Neu umgesetzt: `Management Analyse` im Finance Cockpit hat zusaetzliche Reiter fuer Laender, Datenstatus, Abweichungen, Gutschriften-Kandidaten und Datenqualitaet.
@@ -24,7 +32,56 @@ Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
- Neu umgesetzt und deployed: Finance-Schulung hat einen neuen Tab `Spartenanalyse` mit Navigation, Gruppierung, Top 10, Flaggen, Icons und Statusinterpretation.
- Neu umgesetzt und deployed: Browser-Favicon `wwwroot/favicon.svg` und Head-Link in `Components/App.razor`.
- Letzter Deploy: 2026-05-29 13:47 auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
-- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-favicon` mit `80/80` Tests gruen.
+- Aktueller Stand 2026-06-01 ist validiert, aber noch nicht deployt.
+- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-finance-session-proof` mit `82/82` Tests gruen.
+
+## Nachtrag 2026-06-01 Finance-Sitzung Andreas
+
+Umgesetzt:
+
+- ES hat laut Sitzung keine echte Ist-Abweichung. `DatabaseSeedService` setzt `FinanceReference ES 2025` auf `3'082'320.18 EUR`; `CheckValue` wird fuer ES entfernt.
+- `Tools/FinanceProbe` verwendet fuer den Spain-CSV-Check ebenfalls `3'082'320.18 EUR`.
+- `Settings > Export Einstellungen` hat neu `Wechselkurse anwenden auf` mit Optionen:
+ - `PostingDate / Buchungsdatum`
+ - `InvoiceDate / Rechnungsdatum`
+ - `ExtractionDate / Extraktionsdatum`
+- `Management Analyse > Rohdaten Diagnose` zeigt `Kursdatum` bzw. das fuer Wechselkurse verwendete Datumsfeld.
+- `Management Analyse` erlaubt `CHF` als Anzeige-Waehrung.
+- `Management Analyse > Laender` zeigt zusaetzlich:
+ - `IC/2nd-party`
+ - `Ist ohne IC`
+- Intercompany bleibt Diagnose: Der Standard-Ist wird nicht automatisch bereinigt.
+- Sparten-Zuordnung normalisiert Materialnummern fuer den Vergleich gegen TR-AG-Referenz, insbesondere fuehrende Nullen.
+- Bei >=90% Umsatz in `Nicht zugeordnet` + `Nicht im TR-AG-Stamm` erzeugt die Management-Analyse einen Warnhinweis mit Pruefpunkten (`ProductDivisionRefSet`, Join, fuehrende Nullen, lokale Materialnummern, letzter ZSCHWEIZ-Export).
+- Der Warnhinweis ist per Test `AnalyzeFinanceSummaryAsync_Warns_When_Product_Assignment_Coverage_Is_Implausibly_Low` abgesichert.
+- Bestehender Sparten-Test prueft weiterhin, dass `000MAT-OK` in der TR-AG-Referenz zu `MAT-OK` aus einem lokalen Standort matcht.
+
+Dokumentiert:
+
+- `docs/FINANCE_STATUS_OFFENE_PUNKTE_2026-06-01.md`
+- `docs/FINANCE_MEMO_ANDREAS_2026-06-01.md`
+- `docs/rag/FINANCE.md`
+- `docs/FINANCE_ENTSCHEIDE.md`
+- `docs/FINANCE_BERECHNUNGSFORMELN_LAENDER_2026-05-19.md`
+- `SAGE_SPAIN_EXPORT_2026-05-05.md`
+
+Validierung:
+
+```text
+dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-finance-session-proof
+```
+
+Ergebnis:
+
+```text
+82/82 Tests gruen
+```
+
+Offen / fachlich:
+
+- Pro Standort bestaetigen, ob Intercompany bereits in der gelieferten Quelle herausgerechnet ist.
+- Fuer Wechselkurse fachlich final bestaetigen, welches Datumsfeld fuehrend ist.
+- Falls die Spartenanalyse weiterhin >90% ungeklaert bleibt, TR-AG-Referenz, `ProductDivisionRefSet`, Join und lokale Materialnummern mit Andreas/Kendra pruefen.
## Nachtrag 2026-05-29 Management Analyse UX / Spartenanalyse / Favicon