Update finance session follow-ups

This commit is contained in:
2026-06-01 15:35:23 +02:00
parent 715977beda
commit 6470cb8751
20 changed files with 528 additions and 50 deletions
@@ -152,6 +152,8 @@
<MudTh>@T("Quelle", "Source")</MudTh> <MudTh>@T("Quelle", "Source")</MudTh>
<MudTh>@T("Waehrung", "Currency")</MudTh> <MudTh>@T("Waehrung", "Currency")</MudTh>
<MudTh>@T("Ist", "Actual")</MudTh> <MudTh>@T("Ist", "Actual")</MudTh>
<MudTh>@T("IC/2nd-party", "IC/2nd-party")</MudTh>
<MudTh>@T("Ist ohne IC", "Actual excl. IC")</MudTh>
<MudTh>@T("Soll", "Reference")</MudTh> <MudTh>@T("Soll", "Reference")</MudTh>
<MudTh>@T("Differenz", "Difference")</MudTh> <MudTh>@T("Differenz", "Difference")</MudTh>
<MudTh>@T("Zeilen", "Rows")</MudTh> <MudTh>@T("Zeilen", "Rows")</MudTh>
@@ -163,6 +165,8 @@
<MudTd>@context.SourceSystems</MudTd> <MudTd>@context.SourceSystems</MudTd>
<MudTd>@context.Currency</MudTd> <MudTd>@context.Currency</MudTd>
<MudTd>@FormatValue(context.NetSalesActual, context.Currency)</MudTd> <MudTd>@FormatValue(context.NetSalesActual, context.Currency)</MudTd>
<MudTd>@FormatValue(context.IntercompanyValue, context.Currency)</MudTd>
<MudTd>@FormatValue(context.NetSalesActualExcludingIntercompany, context.Currency)</MudTd>
<MudTd>@FormatNullableValue(context.ReferenceValue, context.Currency)</MudTd> <MudTd>@FormatNullableValue(context.ReferenceValue, context.Currency)</MudTd>
<MudTd>@FormatNullableValue(context.Difference, context.Currency)</MudTd> <MudTd>@FormatNullableValue(context.Difference, context.Currency)</MudTd>
<MudTd>@context.IncludedRows.ToString("N0") / @context.ExcludedRows.ToString("N0")</MudTd> <MudTd>@context.IncludedRows.ToString("N0") / @context.ExcludedRows.ToString("N0")</MudTd>
@@ -720,6 +724,7 @@
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Laender", "Countries")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CountryCount.ToString("N0")</MudText></MudPaper></MudItem> <MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Laender", "Countries")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CountryCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@_centralResult.Summary.ValueFieldLabel</MudText><MudText Typo="Typo.h6">@FormatValue(_centralResult.Summary.ValueTotal, _centralResult.Summary.DisplayCurrency)</MudText></MudPaper></MudItem> <MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@_centralResult.Summary.ValueFieldLabel</MudText><MudText Typo="Typo.h6">@FormatValue(_centralResult.Summary.ValueTotal, _centralResult.Summary.DisplayCurrency)</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Nicht umgerechnet", "Not converted")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.MissingExchangeRateCount.ToString("N0")</MudText></MudPaper></MudItem> <MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Nicht umgerechnet", "Not converted")</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.MissingExchangeRateCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">@T("Kursdatum", "Rate date")</MudText><MudText Typo="Typo.body2">@_centralResult.Summary.ExchangeRateDateLabel</MudText></MudPaper></MudItem>
</MudGrid> </MudGrid>
<MudPaper Class="pa-4 mb-4" Elevation="1"> <MudPaper Class="pa-4 mb-4" Elevation="1">
@@ -881,6 +886,7 @@
private List<ManagementCockpitValueFieldOption> _valueFieldOptions = []; private List<ManagementCockpitValueFieldOption> _valueFieldOptions = [];
private readonly List<CurrencySelectOption> _currencyOptions = private readonly List<CurrencySelectOption> _currencyOptions =
[ [
new(ManagementCockpitCurrencyOptions.Chf, "CHF"),
new(ManagementCockpitCurrencyOptions.Eur, "EUR"), new(ManagementCockpitCurrencyOptions.Eur, "EUR"),
new(ManagementCockpitCurrencyOptions.Usd, "USD"), new(ManagementCockpitCurrencyOptions.Usd, "USD"),
new(ManagementCockpitCurrencyOptions.Native, "Original") new(ManagementCockpitCurrencyOptions.Native, "Original")
@@ -269,6 +269,15 @@
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudSwitch @bind-Value="_exportSettings.TimerEnabled" Label="Timer aktiviert" Color="Color.Primary" /> <MudSwitch @bind-Value="_exportSettings.TimerEnabled" Label="Timer aktiviert" Color="Color.Primary" />
</MudItem> </MudItem>
<MudItem xs="12" md="4">
<MudSelect T="string" @bind-Value="_exportSettings.ExchangeRateDateField"
Label="Wechselkurse anwenden auf"
HelperText="Datumsfeld fuer Kursgueltigkeit in Management-Analysen.">
<MudSelectItem Value="@ExchangeRateDateFields.PostingDate">PostingDate / Buchungsdatum</MudSelectItem>
<MudSelectItem Value="@ExchangeRateDateFields.InvoiceDate">InvoiceDate / Rechnungsdatum</MudSelectItem>
<MudSelectItem Value="@ExchangeRateDateFields.ExtractionDate">ExtractionDate / Extraktionsdatum</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" /> <MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" />
<MudText Typo="Typo.caption"> <MudText Typo="Typo.caption">
@@ -51,6 +51,7 @@ public class ConfigTransferExportSettings
public bool DebugLoggingEnabled { get; set; } public bool DebugLoggingEnabled { get; set; }
public string LocalSiteExportFolder { get; set; } = string.Empty; public string LocalSiteExportFolder { get; set; } = string.Empty;
public string LocalConsolidatedExportFolder { get; set; } = string.Empty; public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
public string ExchangeRateDateField { get; set; } = ExchangeRateDateFields.PostingDate;
} }
public class ConfigTransferCurrencyExchangeRate public class ConfigTransferCurrencyExchangeRate
@@ -10,4 +10,12 @@ public class ExportSettings
public bool DebugLoggingEnabled { get; set; } public bool DebugLoggingEnabled { get; set; }
public string LocalSiteExportFolder { get; set; } = string.Empty; public string LocalSiteExportFolder { get; set; } = string.Empty;
public string LocalConsolidatedExportFolder { 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);
} }
@@ -18,6 +18,7 @@ public static class ManagementCockpitValueFieldKeys
public static class ManagementCockpitCurrencyOptions public static class ManagementCockpitCurrencyOptions
{ {
public const string Native = "NATIVE"; public const string Native = "NATIVE";
public const string Chf = "CHF";
public const string Eur = "EUR"; public const string Eur = "EUR";
public const string Usd = "USD"; public const string Usd = "USD";
} }
@@ -107,6 +108,8 @@ public class ManagementCockpitCentralSummary
public string DisplayCurrency { get; set; } = string.Empty; public string DisplayCurrency { get; set; } = string.Empty;
public decimal ValueTotal { get; set; } public decimal ValueTotal { get; set; }
public int MissingExchangeRateCount { 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? PeriodStart { get; set; }
public DateTime? PeriodEnd { get; set; } public DateTime? PeriodEnd { get; set; }
} }
@@ -175,6 +178,8 @@ public class ManagementFinanceCountryStatusRow : ManagementFinanceSummaryRow
{ {
public string SourceSystems { get; set; } = string.Empty; public string SourceSystems { get; set; } = string.Empty;
public string Tscs { 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? ReferenceValue { get; set; }
public decimal? Difference { get; set; } public decimal? Difference { get; set; }
public decimal? DifferencePercent { get; set; } public decimal? DifferencePercent { get; set; }
@@ -1,16 +1,23 @@
# Sage Spain Export # 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 ## Aktueller Kurzstatus
- Spanien-v2-Export ist technisch lauffaehig und im Testprogramm sichtbar. - Spanien-v2-Export ist technisch lauffaehig und im Testprogramm sichtbar.
- Datei: `sagespain/v2/Spain_Sales_2025.csv` - Datei: `sagespain/v2/Spain_Sales_2025.csv`
- Ist 2025: `3'082'320.18` EUR - Ist 2025: `3'082'320.18` EUR
- Soll aus `check.xlsx`: `3'102'333.61` - Korrigierte Referenz: `3'082'320.18`
- Differenz: `-20'013.43` - Differenz: `0.00`
- Status FinanceProbe: Gelb / Pruefen - Status FinanceProbe: OK, sofern die korrigierte Referenz geladen ist
- Finale Aussage: technisch importierbar, aber fachlich noch nicht abgestimmt. - Finale Aussage: technisch importierbar und laut Sitzung fachlich plausibel; alter Sollwert war falsch.
FinanceProbe lokal: FinanceProbe lokal:
@@ -70,7 +70,8 @@ public class ConfigTransferService : IConfigTransferService
TimerEnabled = exportSettings.TimerEnabled, TimerEnabled = exportSettings.TimerEnabled,
DebugLoggingEnabled = exportSettings.DebugLoggingEnabled, DebugLoggingEnabled = exportSettings.DebugLoggingEnabled,
LocalSiteExportFolder = exportSettings.LocalSiteExportFolder, LocalSiteExportFolder = exportSettings.LocalSiteExportFolder,
LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder,
ExchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(exportSettings.ExchangeRateDateField)
}, },
SourceSystemDefinitions = sourceSystems.Select(system => new ConfigTransferSourceSystemDefinition SourceSystemDefinitions = sourceSystems.Select(system => new ConfigTransferSourceSystemDefinition
{ {
@@ -283,7 +284,8 @@ public class ConfigTransferService : IConfigTransferService
TimerEnabled = importedSettings.TimerEnabled, TimerEnabled = importedSettings.TimerEnabled,
DebugLoggingEnabled = importedSettings.DebugLoggingEnabled, DebugLoggingEnabled = importedSettings.DebugLoggingEnabled,
LocalSiteExportFolder = importedSettings.LocalSiteExportFolder, LocalSiteExportFolder = importedSettings.LocalSiteExportFolder,
LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder,
ExchangeRateDateField = SettingsPageService.NormalizeExchangeRateDateField(importedSettings.ExchangeRateDateField)
}); });
foreach (var sourceSystem in importedSourceSystems) foreach (var sourceSystem in importedSourceSystems)
@@ -27,7 +27,8 @@ CREATE TABLE ExportSettings (
TimerEnabled INTEGER NOT NULL, TimerEnabled INTEGER NOT NULL,
DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0, DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0,
LocalSiteExportFolder TEXT NOT NULL DEFAULT '', 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() => @" internal static string GetHanaServersCreateSql() => @"
@@ -29,6 +29,7 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0"); AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''"); AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "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, "SharePointConfigs", "CentralExportFolder", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''"); AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''");
EnsureTransformationTable(db); EnsureTransformationTable(db);
@@ -57,7 +57,8 @@ public class DatabaseSeedService : IDatabaseSeedService
TimerEnabled = true, TimerEnabled = true,
DebugLoggingEnabled = false, DebugLoggingEnabled = false,
LocalSiteExportFolder = "", LocalSiteExportFolder = "",
LocalConsolidatedExportFolder = "" LocalConsolidatedExportFolder = "",
ExchangeRateDateField = ExchangeRateDateFields.PostingDate
}); });
db.SaveChanges(); db.SaveChanges();
@@ -868,7 +869,7 @@ public class DatabaseSeedService : IDatabaseSeedService
new FinanceReference { Key = "CN", Label = "Trafag CN", Year = 2025 }, new FinanceReference { Key = "CN", Label = "Trafag CN", Year = 2025 },
new FinanceReference { Key = "CZ", Label = "Trafag CZ", Year = 2025, LocalCurrencyValue = 95458782m }, 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 = "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 = "FR", Label = "Trafag FR", Year = 2025, LocalCurrencyValue = 1450582m, CheckValue = 1471218m },
new FinanceReference { Key = "GFS", Label = "Trafag GfS", Year = 2025, LocalCurrencyValue = 6495513m }, new FinanceReference { Key = "GFS", Label = "Trafag GfS", Year = 2025, LocalCurrencyValue = 6495513m },
new FinanceReference { Key = "IN", Label = "Trafag IN", Year = 2025, LocalCurrencyValue = 747341702m, CheckValue = 750936591m }, 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; changed = true;
} }
@@ -184,6 +184,8 @@ public class ManagementCockpitService : IManagementCockpitService
var aggregation = ResolveAggregation(options); var aggregation = ResolveAggregation(options);
using var db = await _dbFactory.CreateDbContextAsync(); 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 var baseRows = await db.CentralSalesRecords
.Select(r => new CentralCockpitRow .Select(r => new CentralCockpitRow
{ {
@@ -196,10 +198,17 @@ public class ManagementCockpitService : IManagementCockpitService
Quantity = r.Quantity, Quantity = r.Quantity,
StandardCost = r.StandardCost, StandardCost = r.StandardCost,
SalesValue = r.SalesPriceValue, 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(); .ToListAsync();
foreach (var row in baseRows)
row.ExchangeRateDate = ResolveExchangeRateDate(exchangeRateDateField, row.PostingDate, row.InvoiceDate, row.ExtractionDate);
if (baseRows.Count == 0) if (baseRows.Count == 0)
throw new InvalidOperationException("Die zentrale Tabelle enthält noch keine Datensätze."); 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)), DisplayCurrency = BuildDisplayCurrencyLabel(selectedRows.Select(x => x.DisplayCurrency)),
ValueTotal = selectedRows.Sum(x => x.Value), ValueTotal = selectedRows.Sum(x => x.Value),
MissingExchangeRateCount = selectedRows.Count(x => x.MissingExchangeRate), MissingExchangeRateCount = selectedRows.Count(x => x.MissingExchangeRate),
ExchangeRateDateField = exchangeRateDateField,
ExchangeRateDateLabel = BuildExchangeRateDateLabel(exchangeRateDateField),
PeriodStart = selectedRows.Min(x => x.PeriodDate), PeriodStart = selectedRows.Min(x => x.PeriodDate),
PeriodEnd = selectedRows.Max(x => x.PeriodDate) PeriodEnd = selectedRows.Max(x => x.PeriodDate)
}, },
AdditionalValueFields = aggregation.AdditionalValueFields AdditionalValueFields = aggregation.AdditionalValueFields
.Select(ToValueFieldOption) .Select(ToValueFieldOption)
.ToList(), .ToList(),
Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate), options), Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate), options, exchangeRateDateField),
YearlyTotals = yearlyRows YearlyTotals = yearlyRows
.GroupBy(x => new { x.PeriodDate.Year, x.DisplayCurrency }) .GroupBy(x => new { x.PeriodDate.Year, x.DisplayCurrency })
.OrderBy(g => g.Key.Year) .OrderBy(g => g.Key.Year)
@@ -316,6 +327,10 @@ public class ManagementCockpitService : IManagementCockpitService
if (financeRules.Count == 0) if (financeRules.Count == 0)
financeRules = FinanceRuleEngine.CreateDefaultRules().ToList(); financeRules = FinanceRuleEngine.CreateDefaultRules().ToList();
var intercompanyRules = await db.FinanceIntercompanyRules
.AsNoTracking()
.Where(rule => rule.IsActive)
.ToListAsync();
var financeRuleEngine = new FinanceRuleEngine(financeRules); var financeRuleEngine = new FinanceRuleEngine(financeRules);
var records = await db.CentralSalesRecords var records = await db.CentralSalesRecords
.AsNoTracking() .AsNoTracking()
@@ -374,6 +389,7 @@ public class ManagementCockpitService : IManagementCockpitService
Include = include, Include = include,
Value = value, Value = value,
RawSalesValue = record.SalesPriceValue, RawSalesValue = record.SalesPriceValue,
IsIntercompany = IsIntercompanyCustomer(record, intercompanyRules),
Quantity = record.Quantity, Quantity = record.Quantity,
InvoiceNumber = record.InvoiceNumber, InvoiceNumber = record.InvoiceNumber,
DocumentType = record.DocumentType, DocumentType = record.DocumentType,
@@ -439,7 +455,8 @@ public class ManagementCockpitService : IManagementCockpitService
"Diese Sicht verwendet dieselbe FinanceRuleEngine wie das zentrale Excel-Blatt Finance Summary.", "Diese Sicht verwendet dieselbe FinanceRuleEngine wie das zentrale Excel-Blatt Finance Summary.",
"Jahr, Land und Waehrung werden auf das Endergebnis angewendet.", "Jahr, Land und Waehrung werden auf das Endergebnis angewendet.",
"Finance-Jahr basiert auf PostingDate, danach InvoiceDate, danach ExtractionDate; DE-Regeln koennen das Jahr erzwingen.", "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) if (scopedRows.Count == 0)
{ {
@@ -461,6 +478,7 @@ public class ManagementCockpitService : IManagementCockpitService
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey); var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
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));
return new ManagementFinanceSummaryResult return new ManagementFinanceSummaryResult
{ {
@@ -578,6 +596,7 @@ public class ManagementCockpitService : IManagementCockpitService
var rowList = group.ToList(); var rowList = group.ToList();
referenceByKey.TryGetValue(group.Key.CountryKey, out var referenceValue); referenceByKey.TryGetValue(group.Key.CountryKey, out var referenceValue);
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 difference = referenceValue.HasValue ? actual - referenceValue.Value : (decimal?)null; var difference = referenceValue.HasValue ? actual - referenceValue.Value : (decimal?)null;
return new ManagementFinanceCountryStatusRow return new ManagementFinanceCountryStatusRow
{ {
@@ -587,6 +606,8 @@ public class ManagementCockpitService : IManagementCockpitService
IncludedRows = rowList.Count(row => row.Include), IncludedRows = rowList.Count(row => row.Include),
ExcludedRows = rowList.Count(row => !row.Include), ExcludedRows = rowList.Count(row => !row.Include),
NetSalesActual = actual, NetSalesActual = actual,
IntercompanyValue = intercompanyValue,
NetSalesActualExcludingIntercompany = actual - intercompanyValue,
SourceSystems = JoinDistinct(rowList.Select(row => row.SourceSystem)), SourceSystems = JoinDistinct(rowList.Select(row => row.SourceSystem)),
Tscs = JoinDistinct(rowList.Select(row => row.Tsc)), Tscs = JoinDistinct(rowList.Select(row => row.Tsc)),
ReferenceValue = referenceValue, ReferenceValue = referenceValue,
@@ -871,7 +892,34 @@ public class ManagementCockpitService : IManagementCockpitService
}; };
private static string NormalizeMaterialKey(string value) 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<string> BuildProductAssignmentNotices(
IReadOnlyCollection<ManagementProductAssignmentRow> 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) private static decimal PercentOf(decimal value, decimal total)
=> total == 0m ? 0m : value * 100m / total; => total == 0m ? 0m : value * 100m / total;
@@ -919,6 +967,43 @@ public class ManagementCockpitService : IManagementCockpitService
return string.Join(", ", reasons.Distinct(StringComparer.OrdinalIgnoreCase)); return string.Join(", ", reasons.Distinct(StringComparer.OrdinalIgnoreCase));
} }
private static bool IsIntercompanyCustomer(SalesRecord record, IReadOnlyList<FinanceIntercompanyRule> 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<string> values) private static string JoinDistinct(IEnumerable<string> values)
{ {
var distinct = values var distinct = values
@@ -1019,7 +1104,7 @@ public class ManagementCockpitService : IManagementCockpitService
.ToList(); .ToList();
var targetCurrency = (options?.TargetCurrency ?? ManagementCockpitCurrencyOptions.Native).Trim().ToUpperInvariant(); 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; targetCurrency = ManagementCockpitCurrencyOptions.Native;
return new AggregationSelection( return new AggregationSelection(
@@ -1047,14 +1132,14 @@ public class ManagementCockpitService : IManagementCockpitService
{ {
var value = ResolveValue(row, aggregation.ValueField); var value = ResolveValue(row, aggregation.ValueField);
var currency = ResolveCurrency(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( var additionalValues = aggregation.AdditionalValueFields.ToDictionary(
field => field.Key, field => field.Key,
field => field =>
{ {
var additionalValue = ResolveValue(row, field); var additionalValue = ResolveValue(row, field);
var additionalCurrency = ResolveCurrency(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); StringComparer.OrdinalIgnoreCase);
@@ -1108,6 +1193,22 @@ public class ManagementCockpitService : IManagementCockpitService
private static string BuildRateCacheKey(string fromCurrency, string toCurrency, DateTime date) private static string BuildRateCacheKey(string fromCurrency, string toCurrency, DateTime date)
=> $"{fromCurrency}|{toCurrency}|{date:yyyy-MM-dd}"; => $"{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) private static decimal ResolveValue(CockpitRow row, ValueFieldDefinition field)
=> field.Key switch => field.Key switch
{ {
@@ -1161,7 +1262,8 @@ public class ManagementCockpitService : IManagementCockpitService
private static List<string> BuildCentralNotices( private static List<string> BuildCentralNotices(
AggregationSelection aggregation, AggregationSelection aggregation,
int missingExchangeRateCount, int missingExchangeRateCount,
ManagementCockpitAnalysisOptions? options) ManagementCockpitAnalysisOptions? options,
string exchangeRateDateField)
{ {
var notices = new List<string> var notices = new List<string>
{ {
@@ -1169,7 +1271,8 @@ public class ManagementCockpitService : IManagementCockpitService
$"Summenfeld: {aggregation.ValueField.Label}.", $"Summenfeld: {aggregation.ValueField.Label}.",
"Keine Intercompany-Bereinigung angewendet.", "Keine Intercompany-Bereinigung angewendet.",
"Kein Budget- und kein Spartemapping 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); var landFilter = NormalizeOptionalFilter(options?.LandFilter);
@@ -1561,7 +1664,11 @@ public class ManagementCockpitService : IManagementCockpitService
public decimal Quantity { get; set; } public decimal Quantity { get; set; }
public decimal StandardCost { get; set; } public decimal StandardCost { get; set; }
public decimal SalesValue { 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 PeriodDate { get; set; }
public DateTime ExchangeRateDate { get; set; }
} }
private class CentralAggregationRow private class CentralAggregationRow
@@ -1588,6 +1695,7 @@ public class ManagementCockpitService : IManagementCockpitService
public bool Include { get; set; } public bool Include { get; set; }
public decimal Value { get; set; } public decimal Value { get; set; }
public decimal RawSalesValue { get; set; } public decimal RawSalesValue { get; set; }
public bool IsIntercompany { get; set; }
public decimal Quantity { get; set; } public decimal Quantity { get; set; }
public string InvoiceNumber { get; set; } = string.Empty; public string InvoiceNumber { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty; public string DocumentType { get; set; } = string.Empty;
@@ -96,6 +96,7 @@ public sealed class SettingsPageService : ISettingsPageService
var existing = await db.ExportSettings.FirstOrDefaultAsync(); var existing = await db.ExportSettings.FirstOrDefaultAsync();
if (existing is null) if (existing is null)
{ {
settings.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
db.ExportSettings.Add(settings); db.ExportSettings.Add(settings);
} }
else else
@@ -107,6 +108,7 @@ public sealed class SettingsPageService : ISettingsPageService
existing.DebugLoggingEnabled = settings.DebugLoggingEnabled; existing.DebugLoggingEnabled = settings.DebugLoggingEnabled;
existing.LocalSiteExportFolder = settings.LocalSiteExportFolder; existing.LocalSiteExportFolder = settings.LocalSiteExportFolder;
existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder; existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder;
existing.ExchangeRateDateField = NormalizeExchangeRateDateField(settings.ExchangeRateDateField);
} }
await db.SaveChangesAsync(); 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 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) public static string BuildSharePointTestPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
{ {
var maskedSecret = string.IsNullOrEmpty(clientSecret) var maskedSecret = string.IsNullOrEmpty(clientSecret)
@@ -556,7 +556,7 @@ static SpainSalesCsvProbe? LoadSpainSalesCsvProbe(string? path)
AddGroupValue(bySeries, series, sales); AddGroupValue(bySeries, series, sales);
} }
const decimal reference = 3102333.61m; const decimal reference = 3082320.18m;
return new SpainSalesCsvProbe return new SpainSalesCsvProbe
{ {
Path = path, Path = path,
@@ -171,6 +171,43 @@ public class ManagementCockpitServiceTests : IDisposable
Assert.Equal(1, exchangeRates.ResolveRateCallCount); 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] [Fact]
public async Task AnalyzeCentralAsync_Can_Sum_Quantity_Without_Currency_Conversion() public async Task AnalyzeCentralAsync_Can_Sum_Quantity_Without_Currency_Conversion()
{ {
@@ -318,7 +355,7 @@ public class ManagementCockpitServiceTests : IDisposable
{ {
await SeedCentralRowsAsync( await SeedCentralRowsAsync(
CreateRow("SAP", "Schweiz", "ZSCHWEIZ", "CH-1", "CHF", 100m, new DateTime(2025, 1, 10), CreateRow("SAP", "Schweiz", "ZSCHWEIZ", "CH-1", "CHF", 100m, new DateTime(2025, 1, 10),
material: "MAT-OK", material: "000MAT-OK",
name: "Reference article", name: "Reference article",
productHierarchyCode: "0414", productHierarchyCode: "0414",
productHierarchyText: "Industat innen", productHierarchyText: "Industat innen",
@@ -393,6 +430,32 @@ public class ManagementCockpitServiceTests : IDisposable
Assert.Equal(80m, deFinanceCoverage.AssignedValuePercent); 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) private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
{ {
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
@@ -440,7 +503,8 @@ public class ManagementCockpitServiceTests : IDisposable
string productFamilyText = "", string productFamilyText = "",
string productDivisionCode = "", string productDivisionCode = "",
string productDivisionText = "", string productDivisionText = "",
string productMappingAssigned = "") string productMappingAssigned = "",
DateTime? postingDate = null)
{ {
return new CentralSalesRecord return new CentralSalesRecord
{ {
@@ -476,6 +540,7 @@ public class ManagementCockpitServiceTests : IDisposable
SalesCurrency = currency, SalesCurrency = currency,
Incoterms2020 = "DAP", Incoterms2020 = "DAP",
SalesResponsibleEmployee = "Alice", SalesResponsibleEmployee = "Alice",
PostingDate = postingDate,
InvoiceDate = invoiceDate, InvoiceDate = invoiceDate,
OrderDate = invoiceDate?.AddDays(-2), OrderDate = invoiceDate?.AddDays(-2),
Land = land, Land = land,
@@ -501,10 +566,12 @@ public class ManagementCockpitServiceTests : IDisposable
private sealed class CountingCurrencyExchangeRateService : ICurrencyExchangeRateService private sealed class CountingCurrencyExchangeRateService : ICurrencyExchangeRateService
{ {
public int ResolveRateCallCount { get; private set; } public int ResolveRateCallCount { get; private set; }
public List<DateTime?> EffectiveDates { get; } = [];
public decimal? ResolveRate(string fromCurrency, string toCurrency, DateTime? effectiveDate) public decimal? ResolveRate(string fromCurrency, string toCurrency, DateTime? effectiveDate)
{ {
ResolveRateCallCount++; ResolveRateCallCount++;
EffectiveDates.Add(effectiveDate);
return 2m; return 2m;
} }
@@ -1,6 +1,12 @@
# Finance Berechnungsformeln pro Land # 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. 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 ```text
Ist ES = Sum(SalesPriceValue) Ist ES = Sum(SalesPriceValue)
Soll ES = 3'102'333.61 EUR Soll ES = 3'082'320.18 EUR
``` ```
Bekannter Stand: Bekannter Stand:
```text ```text
Ist ca. 3'082'320.18 EUR Ist ca. 3'082'320.18 EUR
Differenz ca. -20'013.43 EUR Differenz ca. 0.00 EUR
``` ```
Offen: Offen:
```text ```text
Abweichung ist ca. 0.65%. Die fruehere Abweichung entstand aus einem falschen Soll-/Referenzwert.
Wahrscheinliche Pruefpunkte: Fracht, Portes, Zuschlaege, Rundungen, Versicherung, Falls Audit gefragt ist, muss die Herkunft des alten Werts 3'102'333.61 EUR nachvollzogen werden.
Finanzierung, nicht-artikelbezogene Belegpositionen oder abweichende Rhino-Auswertung.
``` ```
## FR ## FR
+17 -2
View File
@@ -1,6 +1,21 @@
# Finance-Entscheide fuer Net Sales Actuals # 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 ## Nachtrag 2026-05-20 Finance Summary / Management Analyse
@@ -91,7 +106,7 @@ Ergebnis im Reporting:
- IT: IC-Kundenliste final bestaetigen. - IT: IC-Kundenliste final bestaetigen.
- CH / AT: echtes SAP-Buchungsdatum pruefen, falls `ZSCHWEIZ` aktuell nur Fakturadatum liefert. - 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? - 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 ## Pruefstand 2026-05-11
@@ -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.
@@ -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. 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 ## Was aktuell vorhanden ist
### Finance Summary ### 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 | | 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 | | 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 | | 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 | | 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 | | 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. - `ImporteNeto` wird als Nettozeile verwendet.
- Credit Notes / REC laufen negativ. - Credit Notes / REC laufen negativ.
- Ist aktuell ca. `3'082'320.18 EUR`. - Ist aktuell ca. `3'082'320.18 EUR`.
- Sollwert ca. `3'102'333.61 EUR`. - Sitzung 2026-06-01: Dieser Ist-Wert entspricht fachlich dem erwarteten Wert.
- Differenz ca. `-20'013.43 EUR`. - Der bisherige Sollwert ca. `3'102'333.61 EUR` ist falsch bzw. wahrscheinlich ein Excel-/Referenzfehler.
Klaerung: Klaerung:
- Ist `FechaFactura` das richtige Periodendatum? - Falschen Sollwert in `check.xlsx` / FinanceReference korrigieren.
- Sind alle Serien enthalten (`REG`, `LAT`, `PRO`, `REC`)? - Klaeren, woher der falsche Sollwert `3'102'333.61 EUR` kam.
- Gibt es Fracht, Portes, Zuschlaege, Versicherung, Finanzierung oder Nebenpositionen, die Rhino anders behandelt? - Danach ES nicht mehr als fachliche Abweichung fuehren, sofern die Referenz korrigiert ist.
- Gibt es eine offizielle Sage-Auswertung, die den Sollwert erzeugt, inklusive Filterbeschreibung?
Argument: 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 ### 4. UK
@@ -317,6 +347,7 @@ Die Datenquelle ist angebunden. Der kritische Punkt ist, ob Finance die aktuelle
- UK ist Sage, nicht SAP B1. - UK ist Sage, nicht SAP B1.
- DE ist Alphaplan, nicht SAP B1. - DE ist Alphaplan, nicht SAP B1.
- Spartenanalyse nutzt TR-AG-/SAP-Referenz als zentrale Wahrheit. - 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. - Budget-CHF ist Kontrollsicht, nicht Standardabgleich.
- Eine Zahl, die zufaellig naeher am Soll ist, ist nicht automatisch die richtige fachliche Methode. - 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 - Hauswaehrung bleibt fuehrend fuer lokale Freigabe
- CHF als separate Reporting-Sicht - CHF als separate Reporting-Sicht
- offizieller Kurstyp und Kurstabelle - 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: 3. Finance entscheidet den Kostenumfang:
- vorerst keine offizielle Kosten-KPI - 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: 4. Finance priorisiert die offenen Laender:
- DE: Kundenlaender / Filter - DE: Kundenlaender / Filter
- ES: Sage-Differenz - ES: Referenz-/Sollwert korrigieren, keine echte Ist-Abweichung laut Sitzung
- UK: Sage-Differenz - UK: Sage-Differenz
- IT: neuer Export und finale Abgrenzung - IT: neuer Export und finale Abgrenzung
- Spartenanalyse: >90% nicht zugeordnet fachlich unplausibel, Mapping pruefen
5. Finance liefert fuer jede offene Differenz entweder: 5. Finance liefert fuer jede offene Differenz entweder:
- offizielle Reportfilter, - 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? 8. Welcher Kurstyp ist fuer CHF verbindlich?
9. Sollen Kosten jetzt Bestandteil des Finance Dashboards werden oder separat als naechste Ausbaustufe? 9. Sollen Kosten jetzt Bestandteil des Finance Dashboards werden oder separat als naechste Ausbaustufe?
10. Welche Kostenquelle waere fachlich fuehrend? 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 ## Quellen im Repo
+9 -4
View File
@@ -1,6 +1,6 @@
# RAG Finance # RAG Finance
Stand: 2026-05-29 Stand: 2026-06-01
## Kurzstand ## Kurzstand
@@ -15,6 +15,10 @@ Stand: 2026-05-29
- Finance-Schulung dokumentiert die neuen Spartenfunktionen im Tab `Spartenanalyse`. - Finance-Schulung dokumentiert die neuen Spartenfunktionen im Tab `Spartenanalyse`.
- Filter fuer Jahr, Land und Waehrung wirken auf das Finance-Endergebnis. - Filter fuer Jahr, Land und Waehrung wirken auf das Finance-Endergebnis.
- Standard-Ist bleibt inklusive Positionen; Intercompany/2nd-party wird separat ausgewiesen. - 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 ## Wichtige Regeln
@@ -28,12 +32,13 @@ Stand: 2026-05-29
- DE: Finance/Munir muss bestaetigen, welche Kundenlaender/Filter zum offiziellen DE-Ist gehoeren. - 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. - 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 ## Management-Analyse-Reiter
- `Finance Summary`: KPI-Karten und Summen wie im zentralen Excel. - `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. - `Datenstatus`: Standortbestand, letzte Speicherung, letzter Export, Manual-Import-Hinweise.
- `Abweichungen`: Soll/Ist-Abweichungen sortiert nach Betrag. - `Abweichungen`: Soll/Ist-Abweichungen sortiert nach Betrag.
- `Gutschriften`: technische Kandidaten ueber negative Werte und erkennbare Belegtypen/-nummern. - `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 | | CH/AT | SAP OData `ZSCHWEIZ`, Trennung ueber Buchungskreis/Reporting-Land |
| DE | Alphaplan Excel, `NettoPreisGesamtX`, 2025-Zwang | | 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 | | IT | Hauswaehrung, `Trafag Italia` ausgeschlossen, Duplikatlogik fuer leeres Supplier country |
| UK | Sage/Manual Excel, GBP, `[Sales Price/Value] * [Quantity]`, Credit Notes negativ | | UK | Sage/Manual Excel, GBP, `[Sales Price/Value] * [Quantity]`, Credit Notes negativ |
| IN | INR als Hauswaehrung | | IN | INR als Hauswaehrung |
+61 -4
View File
@@ -1,6 +1,6 @@
# Last Change # Last Change
Stand: 2026-05-29 Stand: 2026-06-01
Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert. 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`. - Fuehrender Kurzkontext: `docs/rag/PROJECT.md`.
- Themenrouter: `docs/RAG_ROUTER.md`. - Themenrouter: `docs/RAG_ROUTER.md`.
- Letzter dokumentierter Code-Stand: `36ca822 Add browser favicon`, alle Aenderungen bis 2026-05-29 13:47 deployt. - 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-favicon` mit `80/80` Tests gruen. - 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: 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 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. - 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: 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`. - 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$\`. - 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 ## Nachtrag 2026-05-29 Management Analyse UX / Spartenanalyse / Favicon